cursorflow 2.6.3__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
@@ -18,6 +18,9 @@ from rich.progress import Progress, SpinnerColumn, TextColumn
18
18
  from rich.markup import escape
19
19
 
20
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
21
24
  from . import __version__
22
25
 
23
26
  console = Console()
@@ -293,20 +296,35 @@ def test(base_url, path, actions, output, logs, config, verbose, headless, timeo
293
296
  if timeline:
294
297
  console.print(f"⏰ Timeline events: {len(timeline)}")
295
298
 
296
- # Save results to file for Cursor analysis
297
- if not output:
298
- # Auto-generate meaningful filename in .cursorflow/artifacts/
299
- session_id = results.get('session_id', 'unknown')
300
- path_part = path.replace('/', '_') if path else 'root'
301
-
302
- # Ensure .cursorflow/artifacts directory exists
303
- artifacts_dir = Path('.cursorflow/artifacts')
304
- artifacts_dir.mkdir(parents=True, exist_ok=True)
305
-
306
- 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'
307
302
 
308
- with open(output, 'w') as f:
309
- 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}")
310
328
 
311
329
  # Save command for rerun (Phase 3.3)
312
330
  last_test_data = {
@@ -346,6 +364,320 @@ def test(base_url, path, actions, output, logs, config, verbose, headless, timeo
346
364
  console.print(traceback.format_exc())
347
365
  raise
348
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
+
349
681
  @main.command()
350
682
  @click.argument('mockup_url', required=True)
351
683
  @click.option('--base-url', '-u', default='http://localhost:3000',
@@ -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)}")