cursorflow 2.6.2__py3-none-any.whl → 2.7.1__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/cli.py CHANGED
@@ -15,8 +15,12 @@ from typing import Dict
15
15
  from rich.console import Console
16
16
  from rich.table import Table
17
17
  from rich.progress import Progress, SpinnerColumn, TextColumn
18
+ from rich.markup import escape
18
19
 
19
20
  from .core.agent import TestAgent
21
+ from .core.output_manager import OutputManager
22
+ from .core.data_presenter import DataPresenter
23
+ from .core.query_engine import QueryEngine
20
24
  from . import __version__
21
25
 
22
26
  console = Console()
@@ -185,10 +189,10 @@ def test(base_url, path, actions, output, logs, config, verbose, headless, timeo
185
189
  test_actions = json.loads(actions)
186
190
  console.print(f"📋 Using inline actions")
187
191
  except json.JSONDecodeError as e:
188
- console.print(f"[red]❌ Invalid JSON in actions: {e}[/red]")
192
+ console.print(f"[red]❌ Invalid JSON in actions: {escape(str(e))}[/red]")
189
193
  return
190
194
  except Exception as e:
191
- console.print(f"[red]❌ Failed to load actions: {e}[/red]")
195
+ console.print(f"[red]❌ Failed to load actions: {escape(str(e))}[/red]")
192
196
  return
193
197
  elif path:
194
198
  # Simple path navigation with optional wait conditions
@@ -236,7 +240,7 @@ def test(base_url, path, actions, output, logs, config, verbose, headless, timeo
236
240
  **agent_config
237
241
  )
238
242
  except Exception as e:
239
- console.print(f"[red]Error initializing CursorFlow: {e}[/red]")
243
+ console.print(f"[red]Error initializing CursorFlow: {escape(str(e))}[/red]")
240
244
  return
241
245
 
242
246
  # Execute test actions
@@ -292,20 +296,35 @@ def test(base_url, path, actions, output, logs, config, verbose, headless, timeo
292
296
  if timeline:
293
297
  console.print(f"⏰ Timeline events: {len(timeline)}")
294
298
 
295
- # Save results to file for Cursor analysis
296
- if not output:
297
- # Auto-generate meaningful filename in .cursorflow/artifacts/
298
- session_id = results.get('session_id', 'unknown')
299
- path_part = path.replace('/', '_') if path else 'root'
300
-
301
- # Ensure .cursorflow/artifacts directory exists
302
- artifacts_dir = Path('.cursorflow/artifacts')
303
- artifacts_dir.mkdir(parents=True, exist_ok=True)
304
-
305
- output = artifacts_dir / f"cursorflow_{path_part}_{session_id}.json"
299
+ # Save results in structured multi-file format
300
+ session_id = results.get('session_id', 'unknown')
301
+ test_desc = path if path else 'test'
306
302
 
307
- with open(output, 'w') as f:
308
- json.dump(results, f, indent=2, default=str)
303
+ # Use output manager to save structured results
304
+ output_mgr = OutputManager()
305
+ file_paths = output_mgr.save_structured_results(
306
+ results,
307
+ session_id,
308
+ test_desc
309
+ )
310
+
311
+ # Generate AI-optimized data digest
312
+ session_dir = output_mgr.get_session_path(session_id)
313
+ data_pres = DataPresenter()
314
+ digest_content = data_pres.generate_data_digest(session_dir, results)
315
+
316
+ digest_path = session_dir / "data_digest.md"
317
+ with open(digest_path, 'w', encoding='utf-8') as f:
318
+ f.write(digest_content)
319
+ file_paths['data_digest'] = str(digest_path)
320
+
321
+ if not quiet:
322
+ console.print(f"\n📁 [bold green]Results saved to:[/bold green] [cyan]{session_dir}[/cyan]")
323
+ console.print(f"📄 [bold]AI Summary:[/bold] [cyan]{digest_path}[/cyan]")
324
+ console.print(f"\n💡 [dim]Quick commands:[/dim]")
325
+ console.print(f" cursorflow query {session_id} --errors")
326
+ console.print(f" cursorflow query {session_id} --network")
327
+ console.print(f" cat {digest_path}")
309
328
 
310
329
  # Save command for rerun (Phase 3.3)
311
330
  last_test_data = {
@@ -339,12 +358,326 @@ def test(base_url, path, actions, output, logs, config, verbose, headless, timeo
339
358
  console.print(f"💡 View manually: playwright show-trace {trace_path}")
340
359
 
341
360
  except Exception as e:
342
- console.print(f"[red]❌ Test failed: {e}[/red]")
361
+ console.print(f"[red]❌ Test failed: {escape(str(e))}[/red]")
343
362
  if verbose:
344
363
  import traceback
345
364
  console.print(traceback.format_exc())
346
365
  raise
347
366
 
367
+
368
+ @main.command()
369
+ @click.argument('session_id', required=False)
370
+ @click.option('--errors', is_flag=True, help='Query error data')
371
+ @click.option('--network', is_flag=True, help='Query network requests')
372
+ @click.option('--console', 'console_opt', is_flag=True, help='Query console messages')
373
+ @click.option('--performance', is_flag=True, help='Query performance metrics')
374
+ @click.option('--summary', is_flag=True, help='Query summary data')
375
+ @click.option('--dom', is_flag=True, help='Query DOM analysis')
376
+ @click.option('--server-logs', is_flag=True, help='Query server logs')
377
+ @click.option('--screenshots', is_flag=True, help='Query screenshot metadata')
378
+ @click.option('--mockup', is_flag=True, help='Query mockup comparison results')
379
+ @click.option('--responsive', is_flag=True, help='Query responsive testing results')
380
+ @click.option('--css-iterations', is_flag=True, help='Query CSS iteration results')
381
+ @click.option('--timeline', is_flag=True, help='Query timeline events')
382
+ @click.option('--severity', type=str, help='Filter errors by severity (critical)')
383
+ @click.option('--level', type=str, help='Filter server logs by level (error,warning,info)')
384
+ @click.option('--status', type=str, help='Filter network by status codes (404,500 or 4xx,5xx)')
385
+ @click.option('--failed', is_flag=True, help='Show only failed network requests')
386
+ @click.option('--type', type=str, help='Filter console by type (error,warning,log,info)')
387
+ @click.option('--selector', type=str, help='Filter DOM by CSS selector')
388
+ @click.option('--source', type=str, help='Filter server logs by source (ssh,local,docker,systemd)')
389
+ @click.option('--file', type=str, help='Filter server logs by file path')
390
+ @click.option('--pattern', type=str, help='Filter by content pattern')
391
+ @click.option('--contains', type=str, help='Filter by content substring')
392
+ @click.option('--matches', type=str, help='Filter by regex pattern')
393
+ @click.option('--from-file', type=str, help='Filter errors by source file')
394
+ @click.option('--from-pattern', type=str, help='Filter errors by file pattern (*.js, *.ts)')
395
+ @click.option('--url-contains', type=str, help='Filter network by URL substring')
396
+ @click.option('--url-matches', type=str, help='Filter network by URL regex')
397
+ @click.option('--over', type=str, help='Filter network requests over timing threshold (500ms)')
398
+ @click.option('--method', type=str, help='Filter network by HTTP method (GET,POST)')
399
+ @click.option('--visible', is_flag=True, help='Filter DOM to visible elements only')
400
+ @click.option('--interactive', is_flag=True, help='Filter DOM to interactive elements only')
401
+ @click.option('--role', type=str, help='Filter DOM by ARIA role')
402
+ @click.option('--with-attr', type=str, help='Filter DOM by attribute name')
403
+ @click.option('--with-network', is_flag=True, help='Include related network requests (cross-ref)')
404
+ @click.option('--with-console', is_flag=True, help='Include related console messages (cross-ref)')
405
+ @click.option('--with-server-logs', is_flag=True, help='Include related server logs (cross-ref)')
406
+ @click.option('--context-for-error', type=int, help='Get full context for error by index')
407
+ @click.option('--group-by-url', type=str, help='Group all data by URL pattern')
408
+ @click.option('--group-by-selector', type=str, help='Group all data by DOM selector')
409
+ @click.option('--viewport', type=str, help='Filter responsive results by viewport (mobile,tablet,desktop)')
410
+ @click.option('--iteration', type=int, help='Filter by specific iteration number')
411
+ @click.option('--with-errors', is_flag=True, help='Filter screenshots/iterations with errors only')
412
+ @click.option('--around', type=float, help='Query timeline events around timestamp')
413
+ @click.option('--window', type=float, default=5.0, help='Time window in seconds (default: 5)')
414
+ @click.option('--export', type=click.Choice(['json', 'markdown', 'csv']),
415
+ default='json', help='Export format')
416
+ @click.option('--compare-with', type=str, help='Compare with another session')
417
+ @click.option('--list', 'list_sessions', is_flag=True, help='List recent sessions')
418
+ @click.option('--verbose', '-v', is_flag=True, help='Verbose output')
419
+ def query(session_id, errors, network, console_opt, performance, summary, dom,
420
+ server_logs, screenshots, mockup, responsive, css_iterations, timeline,
421
+ severity, level, status, failed, type, selector, source, file, pattern,
422
+ contains, matches, from_file, from_pattern, url_contains, url_matches, over, method,
423
+ visible, interactive, role, with_attr, with_network, with_console, with_server_logs,
424
+ context_for_error, group_by_url, group_by_selector,
425
+ viewport, iteration, with_errors, around, window,
426
+ export, compare_with, list_sessions, verbose):
427
+ """
428
+ Query and filter CursorFlow test results
429
+
430
+ Examples:
431
+
432
+ # List recent sessions
433
+ cursorflow query --list
434
+
435
+ # Query errors from a session
436
+ cursorflow query session_123 --errors
437
+
438
+ # Query server logs
439
+ cursorflow query session_123 --server-logs --severity error
440
+
441
+ # Query network failures
442
+ cursorflow query session_123 --network --failed
443
+
444
+ # Query responsive results
445
+ cursorflow query session_123 --responsive --viewport mobile
446
+
447
+ # Export in different formats
448
+ cursorflow query session_123 --errors --export markdown
449
+
450
+ # Compare two sessions
451
+ cursorflow query session_123 --compare-with session_456
452
+ """
453
+
454
+ engine = QueryEngine()
455
+
456
+ # List sessions mode
457
+ if list_sessions:
458
+ _list_sessions_func(engine)
459
+ return
460
+
461
+ if not session_id:
462
+ console.print("[yellow]⚠️ Provide a session_id or use --list to see available sessions[/yellow]")
463
+ console.print("Example: [cyan]cursorflow query session_123 --errors[/cyan]")
464
+ return
465
+
466
+ # Comparison mode
467
+ if compare_with:
468
+ _compare_sessions_func(engine, session_id, compare_with, errors, network, performance)
469
+ return
470
+
471
+ # Determine query type
472
+ query_type = None
473
+ if errors:
474
+ query_type = 'errors'
475
+ elif network:
476
+ query_type = 'network'
477
+ elif console_opt:
478
+ query_type = 'console'
479
+ elif performance:
480
+ query_type = 'performance'
481
+ elif summary:
482
+ query_type = 'summary'
483
+ elif dom:
484
+ query_type = 'dom'
485
+ elif server_logs:
486
+ query_type = 'server_logs'
487
+ elif screenshots:
488
+ query_type = 'screenshots'
489
+ elif mockup:
490
+ query_type = 'mockup'
491
+ elif responsive:
492
+ query_type = 'responsive'
493
+ elif css_iterations:
494
+ query_type = 'css_iterations'
495
+ elif timeline:
496
+ query_type = 'timeline'
497
+
498
+ # Build filters
499
+ filters = {}
500
+ if severity:
501
+ filters['severity'] = severity
502
+ if level:
503
+ filters['level'] = level
504
+ if status:
505
+ filters['status'] = status
506
+ if failed:
507
+ filters['failed'] = True
508
+ if type:
509
+ filters['type'] = type
510
+ if selector:
511
+ filters['selector'] = selector
512
+ if source:
513
+ filters['source'] = source
514
+ if file:
515
+ filters['file'] = file
516
+ if pattern:
517
+ filters['pattern'] = pattern
518
+ if contains:
519
+ filters['contains'] = contains
520
+ if matches:
521
+ filters['matches'] = matches
522
+ if from_file:
523
+ filters['from_file'] = from_file
524
+ if from_pattern:
525
+ filters['from_pattern'] = from_pattern
526
+ if url_contains:
527
+ filters['url_contains'] = url_contains
528
+ if url_matches:
529
+ filters['url_matches'] = url_matches
530
+ if over:
531
+ filters['over'] = over
532
+ if method:
533
+ filters['method'] = method
534
+ if visible:
535
+ filters['visible'] = True
536
+ if interactive:
537
+ filters['interactive'] = True
538
+ if role:
539
+ filters['role'] = role
540
+ if with_attr:
541
+ filters['with_attr'] = with_attr
542
+ if with_network:
543
+ filters['with_network'] = True
544
+ if with_console:
545
+ filters['with_console'] = True
546
+ if with_server_logs:
547
+ filters['with_server_logs'] = True
548
+ if context_for_error is not None:
549
+ filters['context_for_error'] = context_for_error
550
+ if group_by_url:
551
+ filters['group_by_url'] = group_by_url
552
+ if group_by_selector:
553
+ filters['group_by_selector'] = group_by_selector
554
+ if viewport:
555
+ filters['viewport'] = viewport
556
+ if iteration:
557
+ filters['iteration'] = iteration
558
+ if with_errors:
559
+ filters['with_errors'] = True
560
+ if around:
561
+ filters['around'] = around
562
+ filters['window'] = window
563
+
564
+ try:
565
+ # Execute query
566
+ result = engine.query_session(session_id, query_type, filters, export)
567
+
568
+ # Display results
569
+ if export == 'json':
570
+ if verbose:
571
+ console.print(result)
572
+ else:
573
+ # Pretty print JSON
574
+ data = json.loads(result)
575
+ console.print_json(data=data)
576
+ else:
577
+ console.print(result)
578
+
579
+ except ValueError as e:
580
+ console.print(f"[red]Error:[/red] {e}")
581
+ console.print(f"\n💡 [dim]Tip:[/dim] List available sessions with: [cyan]cursorflow query --list[/cyan]")
582
+ except Exception as e:
583
+ console.print(f"[red]Error querying session:[/red] {e}")
584
+ if verbose:
585
+ import traceback
586
+ console.print(traceback.format_exc())
587
+
588
+
589
+ def _list_sessions_func(engine: QueryEngine):
590
+ """List recent sessions"""
591
+ sessions = engine.list_sessions(limit=10)
592
+
593
+ if not sessions:
594
+ console.print("[yellow]No sessions found[/yellow]")
595
+ console.print("Run a test first: [cyan]cursorflow test --base-url http://localhost:3000 --path /[/cyan]")
596
+ return
597
+
598
+ table = Table(title="Recent Test Sessions", box=box.ROUNDED)
599
+ table.add_column("Session ID", style="cyan")
600
+ table.add_column("Timestamp", style="white")
601
+ table.add_column("Status", style="green")
602
+ table.add_column("Errors", style="red")
603
+ table.add_column("Network Failures", style="yellow")
604
+
605
+ for session in sessions:
606
+ status_emoji = "✅" if session['success'] else "⚠️"
607
+ table.add_row(
608
+ session['session_id'],
609
+ session['timestamp'],
610
+ status_emoji,
611
+ str(session['errors']),
612
+ str(session['network_failures'])
613
+ )
614
+
615
+ console.print(table)
616
+ console.print(f"\n💡 [dim]Query a session:[/dim] [cyan]cursorflow query <session_id> --errors[/cyan]")
617
+
618
+
619
+ def _compare_sessions_func(engine: QueryEngine, session_a: str, session_b: str,
620
+ errors: bool, network: bool, performance: bool):
621
+ """Compare two sessions"""
622
+ try:
623
+ query_type = None
624
+ if errors:
625
+ query_type = 'errors'
626
+ elif network:
627
+ query_type = 'network'
628
+ elif performance:
629
+ query_type = 'performance'
630
+
631
+ comparison = engine.compare_sessions(session_a, session_b, query_type)
632
+
633
+ console.print(f"\n[bold]Comparing Sessions:[/bold]")
634
+ console.print(f" Session A: [cyan]{session_a}[/cyan]")
635
+ console.print(f" Session B: [cyan]{session_b}[/cyan]")
636
+ console.print()
637
+
638
+ # Display summary comparison
639
+ summary_diff = comparison.get('summary_diff', {})
640
+
641
+ table = Table(title="Summary Comparison", box=box.ROUNDED)
642
+ table.add_column("Metric", style="white")
643
+ table.add_column("Session A", style="cyan")
644
+ table.add_column("Session B", style="cyan")
645
+ table.add_column("Difference", style="yellow")
646
+
647
+ for metric, values in summary_diff.items():
648
+ diff = values.get('difference', 0)
649
+ diff_str = f"+{diff}" if diff > 0 else str(diff)
650
+ table.add_row(
651
+ metric.replace('_', ' ').title(),
652
+ str(values.get('session_a', 0)),
653
+ str(values.get('session_b', 0)),
654
+ diff_str
655
+ )
656
+
657
+ console.print(table)
658
+
659
+ # Display specific comparison if requested
660
+ if errors and 'errors_diff' in comparison:
661
+ console.print(f"\n[bold]Errors Comparison:[/bold]")
662
+ errors_diff = comparison['errors_diff']
663
+ console.print(f" New errors: [red]{errors_diff.get('new_errors', 0)}[/red]")
664
+
665
+ if network and 'network_diff' in comparison:
666
+ console.print(f"\n[bold]Network Comparison:[/bold]")
667
+ network_diff = comparison['network_diff']
668
+ console.print(f" Success rate A: {network_diff.get('success_rate_a', 0):.1f}%")
669
+ console.print(f" Success rate B: {network_diff.get('success_rate_b', 0):.1f}%")
670
+
671
+ if performance and 'performance_diff' in comparison:
672
+ console.print(f"\n[bold]Performance Comparison:[/bold]")
673
+ perf_diff = comparison['performance_diff']
674
+ exec_diff = perf_diff.get('execution_time', {}).get('difference', 0)
675
+ console.print(f" Execution time difference: {exec_diff:.2f}s")
676
+
677
+ except ValueError as e:
678
+ console.print(f"[red]Error:[/red] {e}")
679
+
680
+
348
681
  @main.command()
349
682
  @click.argument('mockup_url', required=True)
350
683
  @click.option('--base-url', '-u', default='http://localhost:3000',
@@ -398,7 +731,7 @@ def compare_mockup(mockup_url, base_url, mockup_actions, implementation_actions,
398
731
  comparison_config["viewports"] = viewports_parsed
399
732
 
400
733
  except Exception as e:
401
- console.print(f"[red]Error parsing input parameters: {e}[/red]")
734
+ console.print(f"[red]Error parsing input parameters: {escape(str(e))}[/red]")
402
735
  return
403
736
 
404
737
  # Initialize CursorFlow
@@ -411,7 +744,7 @@ def compare_mockup(mockup_url, base_url, mockup_actions, implementation_actions,
411
744
  browser_config={'headless': True}
412
745
  )
413
746
  except Exception as e:
414
- console.print(f"[red]Error initializing CursorFlow: {e}[/red]")
747
+ console.print(f"[red]Error initializing CursorFlow: {escape(str(e))}[/red]")
415
748
  return
416
749
 
417
750
  # Execute mockup comparison
@@ -425,7 +758,7 @@ def compare_mockup(mockup_url, base_url, mockup_actions, implementation_actions,
425
758
  ))
426
759
 
427
760
  if "error" in results:
428
- console.print(f"[red]❌ Comparison failed: {results['error']}[/red]")
761
+ console.print(f"[red]❌ Comparison failed: {escape(str(results['error']))}[/red]")
429
762
  return
430
763
 
431
764
  # Display results summary (pure metrics only)
@@ -453,7 +786,7 @@ def compare_mockup(mockup_url, base_url, mockup_actions, implementation_actions,
453
786
  console.print(f"📁 Visual diffs stored in: [cyan].cursorflow/artifacts/[/cyan]")
454
787
 
455
788
  except Exception as e:
456
- console.print(f"[red]❌ Comparison failed: {e}[/red]")
789
+ console.print(f"[red]❌ Comparison failed: {escape(str(e))}[/red]")
457
790
  if verbose:
458
791
  import traceback
459
792
  console.print(traceback.format_exc())
@@ -500,7 +833,7 @@ def iterate_mockup(mockup_url, base_url, css_improvements, base_actions, diff_th
500
833
  comparison_config = {"diff_threshold": diff_threshold}
501
834
 
502
835
  except Exception as e:
503
- console.print(f"[red]Error parsing input parameters: {e}[/red]")
836
+ console.print(f"[red]Error parsing input parameters: {escape(str(e))}[/red]")
504
837
  return
505
838
 
506
839
  # Initialize CursorFlow
@@ -512,7 +845,7 @@ def iterate_mockup(mockup_url, base_url, css_improvements, base_actions, diff_th
512
845
  browser_config={'headless': True}
513
846
  )
514
847
  except Exception as e:
515
- console.print(f"[red]Error initializing CursorFlow: {e}[/red]")
848
+ console.print(f"[red]Error initializing CursorFlow: {escape(str(e))}[/red]")
516
849
  return
517
850
 
518
851
  # Execute iterative mockup matching
@@ -526,7 +859,7 @@ def iterate_mockup(mockup_url, base_url, css_improvements, base_actions, diff_th
526
859
  ))
527
860
 
528
861
  if "error" in results:
529
- console.print(f"[red]❌ Iteration failed: {results['error']}[/red]")
862
+ console.print(f"[red]❌ Iteration failed: {escape(str(results['error']))}[/red]")
530
863
  return
531
864
 
532
865
  # Display results summary
@@ -563,7 +896,7 @@ def iterate_mockup(mockup_url, base_url, css_improvements, base_actions, diff_th
563
896
  console.print(f"📁 Iteration progress stored in: [cyan].cursorflow/artifacts/[/cyan]")
564
897
 
565
898
  except Exception as e:
566
- console.print(f"[red]❌ Iteration failed: {e}[/red]")
899
+ console.print(f"[red]❌ Iteration failed: {escape(str(e))}[/red]")
567
900
  if verbose:
568
901
  import traceback
569
902
  console.print(traceback.format_exc())
@@ -620,7 +953,7 @@ async def _run_auto_tests(framework: str, base_url: str, config: Dict):
620
953
  display_smoke_test_summary(results)
621
954
 
622
955
  except Exception as e:
623
- console.print(f"[red]Auto-test failed: {e}[/red]")
956
+ console.print(f"[red]Auto-test failed: {escape(str(e))}[/red]")
624
957
 
625
958
  @main.command()
626
959
  @click.argument('project_path', default='.')
@@ -648,7 +981,7 @@ def install_rules(project_path, framework, force, yes):
648
981
  console.print("[red]❌ Installation failed[/red]")
649
982
 
650
983
  except Exception as e:
651
- console.print(f"[red]Installation error: {e}[/red]")
984
+ console.print(f"[red]Installation error: {escape(str(e))}[/red]")
652
985
 
653
986
  @main.command()
654
987
  @click.option('--force', is_flag=True, help='Force update even if no updates available')
@@ -670,7 +1003,7 @@ def update(force, project_dir):
670
1003
  console.print("[red]❌ Update failed[/red]")
671
1004
 
672
1005
  except Exception as e:
673
- console.print(f"[red]Update error: {e}[/red]")
1006
+ console.print(f"[red]Update error: {escape(str(e))}[/red]")
674
1007
 
675
1008
  @main.command()
676
1009
  @click.option('--project-dir', default='.', help='Project directory')
@@ -684,7 +1017,7 @@ def check_updates(project_dir):
684
1017
  result = asyncio.run(check_updates(project_dir))
685
1018
 
686
1019
  if "error" in result:
687
- console.print(f"[red]Error checking updates: {result['error']}[/red]")
1020
+ console.print(f"[red]Error checking updates: {escape(str(result['error']))}[/red]")
688
1021
  return
689
1022
 
690
1023
  # Display update information
@@ -723,7 +1056,7 @@ def check_updates(project_dir):
723
1056
  console.print("\n💡 Run [bold]cursorflow update[/bold] to install updates")
724
1057
 
725
1058
  except Exception as e:
726
- console.print(f"[red]Error: {e}[/red]")
1059
+ console.print(f"[red]Error: {escape(str(e))}[/red]")
727
1060
 
728
1061
  @main.command()
729
1062
  @click.option('--project-dir', default='.', help='Project directory')
@@ -744,7 +1077,7 @@ def install_deps(project_dir):
744
1077
  console.print("[red]❌ Dependency installation failed[/red]")
745
1078
 
746
1079
  except Exception as e:
747
- console.print(f"[red]Error: {e}[/red]")
1080
+ console.print(f"[red]Error: {escape(str(e))}[/red]")
748
1081
 
749
1082
  @main.command()
750
1083
  @click.argument('subcommand', required=False)
@@ -959,7 +1292,7 @@ def inspect(base_url, path, selector, verbose):
959
1292
  console.print(f"\n📸 Screenshot saved: [cyan]{screenshot_path}[/cyan]")
960
1293
 
961
1294
  except Exception as e:
962
- console.print(f"[red]❌ Inspection failed: {e}[/red]")
1295
+ console.print(f"[red]❌ Inspection failed: {escape(str(e))}[/red]")
963
1296
  import traceback
964
1297
  console.print(traceback.format_exc())
965
1298
 
@@ -1086,7 +1419,7 @@ def measure(base_url, path, selector, verbose):
1086
1419
  console.print("✅ Measurement complete")
1087
1420
 
1088
1421
  except Exception as e:
1089
- console.print(f"[red]❌ Measurement failed: {e}[/red]")
1422
+ console.print(f"[red]❌ Measurement failed: {escape(str(e))}[/red]")
1090
1423
  import traceback
1091
1424
  console.print(traceback.format_exc())
1092
1425
 
@@ -1137,7 +1470,7 @@ def count(base_url, path, selector):
1137
1470
  console.print(f"💡 Total elements on page: {len(elements)}")
1138
1471
 
1139
1472
  except Exception as e:
1140
- console.print(f"[red]❌ Count failed: {e}[/red]")
1473
+ console.print(f"[red]❌ Count failed: {escape(str(e))}[/red]")
1141
1474
  import traceback
1142
1475
  console.print(traceback.format_exc())
1143
1476
 
@@ -1174,7 +1507,7 @@ def rerun(click, hover):
1174
1507
  console.print("✅ Rerun completed")
1175
1508
 
1176
1509
  except Exception as e:
1177
- console.print(f"[red]❌ Rerun failed: {e}[/red]")
1510
+ console.print(f"[red]❌ Rerun failed: {escape(str(e))}[/red]")
1178
1511
 
1179
1512
  @main.command()
1180
1513
  @click.option('--session', '-s', required=True, help='Session ID to view timeline for')
@@ -1227,7 +1560,7 @@ def timeline(session):
1227
1560
  console.print(f"\n... and {len(timeline) - 50} more events")
1228
1561
 
1229
1562
  except Exception as e:
1230
- console.print(f"[red]❌ Failed to load timeline: {e}[/red]")
1563
+ console.print(f"[red]❌ Failed to load timeline: {escape(str(e))}[/red]")
1231
1564
 
1232
1565
  @main.command()
1233
1566
  @click.option('--artifacts', is_flag=True, help='Clean all artifacts (screenshots, traces)')
@@ -1337,7 +1670,7 @@ def cleanup(artifacts, sessions, old_only, clean_all, dry_run, yes):
1337
1670
  item_path.unlink()
1338
1671
  deleted_count += 1
1339
1672
  except Exception as e:
1340
- console.print(f"[red]⚠️ Failed to delete {item_path}: {e}[/red]")
1673
+ console.print(f"[red]⚠️ Failed to delete {item_path}: {escape(str(e))}[/red]")
1341
1674
 
1342
1675
  console.print(f"\n✅ Cleanup complete!")
1343
1676
  console.print(f" • Deleted {deleted_count}/{len(items_to_delete)} items")
@@ -1418,7 +1751,7 @@ def _display_test_results(results: Dict, test_description: str, show_console: bo
1418
1751
  if errors:
1419
1752
  console.print(f"\n[red]❌ Console Errors ({len(errors)}):[/red]")
1420
1753
  for error in errors[:5]: # Show first 5
1421
- console.print(f" [red]{error.get('text', 'Unknown error')}[/red]")
1754
+ console.print(f" [red]{escape(str(error.get('text', 'Unknown error')))}[/red]")
1422
1755
 
1423
1756
  if warnings:
1424
1757
  console.print(f"\n[yellow]⚠️ Console Warnings ({len(warnings)}):[/yellow]")
@@ -21,6 +21,8 @@ from .css_iterator import CSSIterator
21
21
  from .cursor_integration import CursorIntegration
22
22
  from .persistent_session import PersistentSession, get_session_manager
23
23
  from .mockup_comparator import MockupComparator
24
+ from .output_manager import OutputManager
25
+ from .data_presenter import DataPresenter
24
26
 
25
27
 
26
28
  class CursorFlow:
@@ -74,6 +76,10 @@ class CursorFlow:
74
76
  self.cursor_integration = CursorIntegration()
75
77
  self.mockup_comparator = MockupComparator()
76
78
 
79
+ # Initialize output manager and data presenter
80
+ self.output_manager = OutputManager()
81
+ self.data_presenter = DataPresenter()
82
+
77
83
  # Session tracking
78
84
  self.session_id = None
79
85
  self.timeline = []
@@ -166,7 +172,8 @@ class CursorFlow:
166
172
  "server_logs": server_logs, # Raw server logs
167
173
  "summary": summary, # Basic counts
168
174
  "artifacts": self.artifacts,
169
- "comprehensive_data": comprehensive_data # Complete page intelligence
175
+ "comprehensive_data": comprehensive_data, # Complete page intelligence
176
+ "test_description": session_options.get('test_description', 'test') if session_options else 'test'
170
177
  }
171
178
 
172
179
  self.logger.info(f"Test execution completed: {success}, timeline events: {len(organized_timeline)}")