cursorflow 1.3.6__py3-none-any.whl → 2.0.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,439 @@
1
+ """
2
+ CursorFlow v2.0 Hot Module Replacement (HMR) Detection System
3
+
4
+ This module provides intelligent detection and monitoring of Hot Module Replacement
5
+ events across different development frameworks, enabling precision timing for CSS
6
+ iteration workflows.
7
+
8
+ Core Philosophy: Pure observation of HMR events - we listen but never trigger.
9
+ """
10
+
11
+ import asyncio
12
+ import json
13
+ import logging
14
+ import time
15
+ from typing import Dict, List, Optional, Any, Callable
16
+ from urllib.parse import urlparse
17
+ import websockets
18
+ from websockets.exceptions import ConnectionClosed, InvalidURI
19
+
20
+
21
+ class HMRDetector:
22
+ """
23
+ v2.0 Enhancement: Intelligent Hot Module Replacement event detection
24
+
25
+ Supports auto-detection and monitoring for:
26
+ - Vite (port 5173, WebSocket path /__vite_hmr)
27
+ - Webpack Dev Server (port 3000, WebSocket path /sockjs-node)
28
+ - Next.js (port 3000, WebSocket path /_next/webpack-hmr)
29
+ - Parcel (port 1234, WebSocket path /hmr)
30
+ - Laravel Mix (port 3000, WebSocket path /browser-sync/socket.io)
31
+ """
32
+
33
+ def __init__(self, base_url: str):
34
+ self.base_url = base_url
35
+ self.logger = logging.getLogger(self.__class__.__name__)
36
+
37
+ # Parse the base URL to get host and port
38
+ parsed_url = urlparse(base_url)
39
+ self.host = parsed_url.hostname or 'localhost'
40
+ self.port = parsed_url.port or 3000
41
+
42
+ # HMR detection state
43
+ self.detected_framework = None
44
+ self.hmr_config = None
45
+ self.websocket = None
46
+ self.is_monitoring = False
47
+ self.hmr_events = []
48
+ self.event_callbacks = []
49
+
50
+ # Framework configurations
51
+ self.framework_configs = {
52
+ 'vite': {
53
+ 'name': 'Vite',
54
+ 'default_port': 5173,
55
+ 'ws_paths': ['/__vite_hmr', '/vite-hmr'],
56
+ 'event_patterns': ['vite:beforeUpdate', 'vite:afterUpdate', 'css-update'],
57
+ 'css_update_indicators': ['css-update', 'style-update']
58
+ },
59
+ 'webpack': {
60
+ 'name': 'Webpack Dev Server',
61
+ 'default_port': 3000,
62
+ 'ws_paths': ['/sockjs-node', '/webpack-hmr', '/ws'],
63
+ 'event_patterns': ['webpackHotUpdate', 'hot', 'hash'],
64
+ 'css_update_indicators': ['css', 'style', 'hot-update']
65
+ },
66
+ 'nextjs': {
67
+ 'name': 'Next.js',
68
+ 'default_port': 3000,
69
+ 'ws_paths': ['/_next/webpack-hmr', '/_next/static/hmr'],
70
+ 'event_patterns': ['building', 'built', 'sync'],
71
+ 'css_update_indicators': ['css', 'style']
72
+ },
73
+ 'parcel': {
74
+ 'name': 'Parcel',
75
+ 'default_port': 1234,
76
+ 'ws_paths': ['/hmr', '/parcel-hmr'],
77
+ 'event_patterns': ['buildSuccess', 'buildError', 'hmr:update'],
78
+ 'css_update_indicators': ['css', 'style']
79
+ },
80
+ 'laravel_mix': {
81
+ 'name': 'Laravel Mix',
82
+ 'default_port': 3000,
83
+ 'ws_paths': ['/browser-sync/socket.io', '/browsersync'],
84
+ 'event_patterns': ['file:changed', 'browser:reload'],
85
+ 'css_update_indicators': ['css', 'scss', 'sass']
86
+ }
87
+ }
88
+
89
+ async def auto_detect_framework(self) -> Optional[str]:
90
+ """
91
+ Auto-detect the development framework by probing WebSocket endpoints
92
+
93
+ Returns the detected framework key or None if no framework detected
94
+ """
95
+ self.logger.info(f"Auto-detecting HMR framework for {self.base_url}")
96
+
97
+ # Try to detect based on port first
98
+ port_hints = {
99
+ 5173: ['vite'],
100
+ 3000: ['webpack', 'nextjs', 'laravel_mix'],
101
+ 1234: ['parcel']
102
+ }
103
+
104
+ frameworks_to_check = port_hints.get(self.port, list(self.framework_configs.keys()))
105
+
106
+ for framework_key in frameworks_to_check:
107
+ config = self.framework_configs[framework_key]
108
+
109
+ # Test WebSocket connections for this framework
110
+ for ws_path in config['ws_paths']:
111
+ ws_url = f"ws://{self.host}:{self.port}{ws_path}"
112
+
113
+ try:
114
+ self.logger.debug(f"Testing WebSocket connection: {ws_url}")
115
+
116
+ # Quick connection test with timeout
117
+ websocket = await asyncio.wait_for(
118
+ websockets.connect(ws_url, ping_interval=None),
119
+ timeout=2.0
120
+ )
121
+
122
+ # Connection successful - this is likely our framework
123
+ await websocket.close()
124
+
125
+ self.detected_framework = framework_key
126
+ self.hmr_config = {
127
+ 'framework': framework_key,
128
+ 'name': config['name'],
129
+ 'ws_url': ws_url,
130
+ 'ws_path': ws_path,
131
+ 'event_patterns': config['event_patterns'],
132
+ 'css_indicators': config['css_update_indicators']
133
+ }
134
+
135
+ self.logger.info(f"✅ Detected HMR framework: {config['name']} at {ws_url}")
136
+ return framework_key
137
+
138
+ except (ConnectionClosed, InvalidURI, OSError, asyncio.TimeoutError) as e:
139
+ self.logger.debug(f"WebSocket test failed for {ws_url}: {e}")
140
+ continue
141
+
142
+ self.logger.warning("❌ No HMR framework detected - CSS iteration will use fallback timing")
143
+ return None
144
+
145
+ async def start_monitoring(self, on_hmr_event: Optional[Callable] = None) -> bool:
146
+ """
147
+ Start monitoring HMR events
148
+
149
+ Args:
150
+ on_hmr_event: Optional callback function for HMR events
151
+
152
+ Returns:
153
+ True if monitoring started successfully, False otherwise
154
+ """
155
+ if not self.hmr_config:
156
+ await self.auto_detect_framework()
157
+
158
+ if not self.hmr_config:
159
+ self.logger.warning("Cannot start HMR monitoring - no framework detected")
160
+ return False
161
+
162
+ try:
163
+ ws_url = self.hmr_config['ws_url']
164
+ self.logger.info(f"Starting HMR monitoring for {self.hmr_config['name']} at {ws_url}")
165
+
166
+ self.websocket = await websockets.connect(ws_url, ping_interval=20)
167
+ self.is_monitoring = True
168
+
169
+ if on_hmr_event:
170
+ self.event_callbacks.append(on_hmr_event)
171
+
172
+ # Start the monitoring loop
173
+ asyncio.create_task(self._monitor_hmr_events())
174
+
175
+ self.logger.info("✅ HMR monitoring started successfully")
176
+ return True
177
+
178
+ except Exception as e:
179
+ self.logger.error(f"Failed to start HMR monitoring: {e}")
180
+ self.is_monitoring = False
181
+ return False
182
+
183
+ async def _monitor_hmr_events(self):
184
+ """Internal method to monitor WebSocket messages for HMR events"""
185
+ try:
186
+ async for message in self.websocket:
187
+ await self._process_hmr_message(message)
188
+
189
+ except ConnectionClosed:
190
+ self.logger.info("HMR WebSocket connection closed")
191
+ self.is_monitoring = False
192
+ except Exception as e:
193
+ self.logger.error(f"HMR monitoring error: {e}")
194
+ self.is_monitoring = False
195
+
196
+ async def _process_hmr_message(self, message: str):
197
+ """Process incoming HMR WebSocket messages"""
198
+ try:
199
+ # Parse message (could be JSON or plain text depending on framework)
200
+ try:
201
+ data = json.loads(message)
202
+ message_type = 'json'
203
+ except json.JSONDecodeError:
204
+ data = message
205
+ message_type = 'text'
206
+
207
+ # Create HMR event record
208
+ hmr_event = {
209
+ 'timestamp': time.time(),
210
+ 'framework': self.hmr_config['framework'],
211
+ 'message_type': message_type,
212
+ 'raw_message': message,
213
+ 'parsed_data': data if message_type == 'json' else None,
214
+ 'event_type': self._classify_hmr_event(data, message),
215
+ 'is_css_update': self._is_css_update(data, message)
216
+ }
217
+
218
+ # Store the event
219
+ self.hmr_events.append(hmr_event)
220
+
221
+ # Keep only last 100 events to prevent memory issues
222
+ if len(self.hmr_events) > 100:
223
+ self.hmr_events = self.hmr_events[-100:]
224
+
225
+ # Notify callbacks
226
+ for callback in self.event_callbacks:
227
+ try:
228
+ await callback(hmr_event)
229
+ except Exception as e:
230
+ self.logger.error(f"HMR event callback error: {e}")
231
+
232
+ # Log important events
233
+ if hmr_event['is_css_update']:
234
+ self.logger.info(f"🎨 CSS update detected: {hmr_event['event_type']}")
235
+ elif hmr_event['event_type'] != 'heartbeat':
236
+ self.logger.debug(f"HMR event: {hmr_event['event_type']}")
237
+
238
+ except Exception as e:
239
+ self.logger.error(f"Error processing HMR message: {e}")
240
+
241
+ def _classify_hmr_event(self, data: Any, raw_message: str) -> str:
242
+ """Classify the type of HMR event based on message content"""
243
+ if not self.hmr_config:
244
+ return 'unknown'
245
+
246
+ framework = self.hmr_config['framework']
247
+ event_patterns = self.hmr_config['event_patterns']
248
+
249
+ # Convert to string for pattern matching
250
+ message_str = str(data).lower() if data else raw_message.lower()
251
+
252
+ # Framework-specific event classification
253
+ if framework == 'vite':
254
+ if 'vite:beforeupdate' in message_str:
255
+ return 'build_start'
256
+ elif 'vite:afterupdate' in message_str or 'css-update' in message_str:
257
+ return 'css_update'
258
+ elif 'connected' in message_str:
259
+ return 'connection'
260
+ elif 'ping' in message_str or 'pong' in message_str:
261
+ return 'heartbeat'
262
+
263
+ elif framework == 'webpack':
264
+ if 'webpackhotupdate' in message_str:
265
+ return 'hot_update'
266
+ elif 'building' in message_str:
267
+ return 'build_start'
268
+ elif 'built' in message_str:
269
+ return 'build_complete'
270
+ elif 'hash' in message_str:
271
+ return 'hash_update'
272
+
273
+ elif framework == 'nextjs':
274
+ if 'building' in message_str:
275
+ return 'build_start'
276
+ elif 'built' in message_str:
277
+ return 'build_complete'
278
+ elif 'sync' in message_str:
279
+ return 'sync'
280
+
281
+ elif framework == 'parcel':
282
+ if 'buildsuccess' in message_str:
283
+ return 'build_success'
284
+ elif 'builderror' in message_str:
285
+ return 'build_error'
286
+ elif 'hmr:update' in message_str:
287
+ return 'hmr_update'
288
+
289
+ elif framework == 'laravel_mix':
290
+ if 'file:changed' in message_str:
291
+ return 'file_change'
292
+ elif 'browser:reload' in message_str:
293
+ return 'browser_reload'
294
+
295
+ # Check against general patterns
296
+ for pattern in event_patterns:
297
+ if pattern.lower() in message_str:
298
+ return pattern
299
+
300
+ return 'unknown'
301
+
302
+ def _is_css_update(self, data: Any, raw_message: str) -> bool:
303
+ """Determine if this HMR event represents a CSS update"""
304
+ if not self.hmr_config:
305
+ return False
306
+
307
+ css_indicators = self.hmr_config['css_indicators']
308
+ message_str = str(data).lower() if data else raw_message.lower()
309
+
310
+ # Check for CSS-specific indicators
311
+ for indicator in css_indicators:
312
+ if indicator in message_str:
313
+ return True
314
+
315
+ # Additional CSS detection patterns
316
+ css_patterns = ['.css', '.scss', '.sass', '.less', 'style', 'stylesheet']
317
+ for pattern in css_patterns:
318
+ if pattern in message_str:
319
+ return True
320
+
321
+ return False
322
+
323
+ async def wait_for_css_update(self, timeout: float = 10.0) -> Optional[Dict]:
324
+ """
325
+ Wait for the next CSS update event with precision timing
326
+
327
+ This is the key method that replaces arbitrary waits in CSS iteration:
328
+
329
+ OLD WAY:
330
+ await page.screenshot("before.png")
331
+ # ... developer makes CSS changes ...
332
+ await page.wait_for_timeout(2000) # Arbitrary wait
333
+ await page.screenshot("after.png")
334
+
335
+ NEW WAY:
336
+ await page.screenshot("before.png")
337
+ # ... developer makes CSS changes ...
338
+ css_event = await hmr_detector.wait_for_css_update() # Precise timing
339
+ await page.screenshot("after.png")
340
+
341
+ Args:
342
+ timeout: Maximum time to wait for CSS update (seconds)
343
+
344
+ Returns:
345
+ HMR event dict if CSS update detected, None if timeout
346
+ """
347
+ if not self.is_monitoring:
348
+ self.logger.warning("HMR monitoring not active - cannot wait for CSS updates")
349
+ return None
350
+
351
+ start_time = time.time()
352
+ initial_event_count = len(self.hmr_events)
353
+
354
+ self.logger.info(f"⏱️ Waiting for CSS update (timeout: {timeout}s)")
355
+
356
+ while time.time() - start_time < timeout:
357
+ # Check for new CSS update events
358
+ for event in self.hmr_events[initial_event_count:]:
359
+ if event['is_css_update']:
360
+ self.logger.info(f"✅ CSS update detected after {time.time() - start_time:.2f}s")
361
+ return event
362
+
363
+ # Short sleep to prevent busy waiting
364
+ await asyncio.sleep(0.1)
365
+
366
+ self.logger.warning(f"⏰ CSS update wait timeout after {timeout}s")
367
+ return None
368
+
369
+ async def wait_for_build_complete(self, timeout: float = 30.0) -> Optional[Dict]:
370
+ """
371
+ Wait for build completion (useful for more complex changes)
372
+
373
+ Args:
374
+ timeout: Maximum time to wait for build completion (seconds)
375
+
376
+ Returns:
377
+ HMR event dict if build completed, None if timeout
378
+ """
379
+ if not self.is_monitoring:
380
+ self.logger.warning("HMR monitoring not active - cannot wait for build completion")
381
+ return None
382
+
383
+ start_time = time.time()
384
+ initial_event_count = len(self.hmr_events)
385
+
386
+ self.logger.info(f"⏱️ Waiting for build completion (timeout: {timeout}s)")
387
+
388
+ build_complete_indicators = ['build_complete', 'build_success', 'css_update', 'hot_update']
389
+
390
+ while time.time() - start_time < timeout:
391
+ # Check for build completion events
392
+ for event in self.hmr_events[initial_event_count:]:
393
+ if event['event_type'] in build_complete_indicators:
394
+ self.logger.info(f"✅ Build completed after {time.time() - start_time:.2f}s")
395
+ return event
396
+
397
+ await asyncio.sleep(0.1)
398
+
399
+ self.logger.warning(f"⏰ Build completion wait timeout after {timeout}s")
400
+ return None
401
+
402
+ async def stop_monitoring(self):
403
+ """Stop HMR monitoring and close WebSocket connection"""
404
+ self.is_monitoring = False
405
+
406
+ if self.websocket:
407
+ try:
408
+ await self.websocket.close()
409
+ self.logger.info("HMR monitoring stopped")
410
+ except Exception as e:
411
+ self.logger.error(f"Error stopping HMR monitoring: {e}")
412
+
413
+ self.websocket = None
414
+
415
+ def get_hmr_status(self) -> Dict[str, Any]:
416
+ """Get current HMR detection and monitoring status"""
417
+ return {
418
+ 'framework_detected': self.detected_framework,
419
+ 'framework_name': self.hmr_config['name'] if self.hmr_config else None,
420
+ 'is_monitoring': self.is_monitoring,
421
+ 'websocket_url': self.hmr_config['ws_url'] if self.hmr_config else None,
422
+ 'total_events': len(self.hmr_events),
423
+ 'css_events': len([e for e in self.hmr_events if e['is_css_update']]),
424
+ 'recent_events': self.hmr_events[-5:] if self.hmr_events else []
425
+ }
426
+
427
+ def get_framework_info(self) -> Dict[str, Any]:
428
+ """Get information about the detected framework"""
429
+ if not self.hmr_config:
430
+ return {'detected': False, 'supported_frameworks': list(self.framework_configs.keys())}
431
+
432
+ return {
433
+ 'detected': True,
434
+ 'framework': self.hmr_config['framework'],
435
+ 'name': self.hmr_config['name'],
436
+ 'websocket_path': self.hmr_config['ws_path'],
437
+ 'css_indicators': self.hmr_config['css_indicators'],
438
+ 'event_patterns': self.hmr_config['event_patterns']
439
+ }
@@ -11,8 +11,20 @@ import json
11
11
  from typing import Dict, List, Optional, Any, Tuple
12
12
  from pathlib import Path
13
13
  import logging
14
- from PIL import Image, ImageDraw, ImageChops
15
- import numpy as np
14
+ try:
15
+ from PIL import Image, ImageDraw, ImageChops
16
+ import numpy as np
17
+ VISUAL_COMPARISON_AVAILABLE = True
18
+ # Type aliases for when PIL is available
19
+ PILImage = Image.Image
20
+ NDArray = np.ndarray
21
+ except ImportError:
22
+ # PIL/numpy not available - visual comparison features disabled
23
+ Image = ImageDraw = ImageChops = np = None
24
+ VISUAL_COMPARISON_AVAILABLE = False
25
+ # Dummy types for when PIL/numpy are not available
26
+ PILImage = Any
27
+ NDArray = Any
16
28
 
17
29
  from .browser_controller import BrowserController
18
30
  from .css_iterator import CSSIterator
@@ -327,6 +339,20 @@ class MockupComparator:
327
339
  config: Dict
328
340
  ) -> Dict[str, Any]:
329
341
  """Create visual difference analysis between mockup and implementation"""
342
+
343
+ if not VISUAL_COMPARISON_AVAILABLE:
344
+ self.logger.warning("Visual comparison unavailable - PIL/numpy not installed")
345
+ return {
346
+ "error": "Visual comparison requires PIL and numpy",
347
+ "diff_path": None,
348
+ "highlighted_path": None,
349
+ "similarity_score": 0.0,
350
+ "difference_areas": [],
351
+ "pixel_differences": 0,
352
+ "total_pixels": 0,
353
+ "note": "Install with: pip install pillow numpy"
354
+ }
355
+
330
356
  try:
331
357
  # Load images
332
358
  mockup_img = Image.open(mockup_path)
@@ -369,7 +395,7 @@ class MockupComparator:
369
395
  self.logger.error(f"Visual diff creation failed: {e}")
370
396
  return {"error": str(e)}
371
397
 
372
- def _create_highlighted_diff(self, mockup_img: Image.Image, implementation_img: Image.Image, diff_img: Image.Image, config: Dict) -> Image.Image:
398
+ def _create_highlighted_diff(self, mockup_img: PILImage, implementation_img: PILImage, diff_img: PILImage, config: Dict) -> PILImage:
373
399
  """Create highlighted difference image with colored regions"""
374
400
  # Convert to RGBA for overlay
375
401
  highlighted = implementation_img.convert("RGBA")
@@ -393,7 +419,7 @@ class MockupComparator:
393
419
 
394
420
  return highlighted.convert("RGB")
395
421
 
396
- def _calculate_visual_diff_metrics(self, mockup_img: Image.Image, implementation_img: Image.Image, diff_img: Image.Image, config: Dict) -> Dict[str, Any]:
422
+ def _calculate_visual_diff_metrics(self, mockup_img: PILImage, implementation_img: PILImage, diff_img: PILImage, config: Dict) -> Dict[str, Any]:
397
423
  """Calculate detailed visual difference metrics"""
398
424
  try:
399
425
  # Convert to numpy arrays for analysis
@@ -426,7 +452,7 @@ class MockupComparator:
426
452
  self.logger.error(f"Visual diff metrics calculation failed: {e}")
427
453
  return {"error": str(e)}
428
454
 
429
- def _find_difference_regions(self, diff_array: np.ndarray, threshold: float) -> List[Dict]:
455
+ def _find_difference_regions(self, diff_array: NDArray, threshold: float) -> List[Dict]:
430
456
  """Find regions with significant visual differences"""
431
457
  # This is a simplified implementation - could be enhanced with computer vision
432
458
  significant_diff = diff_array > threshold
@@ -448,7 +474,7 @@ class MockupComparator:
448
474
 
449
475
  return regions
450
476
 
451
- def _calculate_color_metrics(self, mockup_array: np.ndarray, implementation_array: np.ndarray) -> Dict[str, Any]:
477
+ def _calculate_color_metrics(self, mockup_array: NDArray, implementation_array: NDArray) -> Dict[str, Any]:
452
478
  """Calculate color-based difference metrics"""
453
479
  try:
454
480
  # Calculate average colors