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 +345 -13
- cursorflow/core/cursorflow.py +8 -1
- cursorflow/core/data_presenter.py +518 -0
- cursorflow/core/output_manager.py +523 -0
- cursorflow/core/query_engine.py +1345 -0
- cursorflow/rules/cursorflow-usage.mdc +245 -6
- {cursorflow-2.6.3.dist-info → cursorflow-2.7.1.dist-info}/METADATA +59 -1
- {cursorflow-2.6.3.dist-info → cursorflow-2.7.1.dist-info}/RECORD +12 -9
- {cursorflow-2.6.3.dist-info → cursorflow-2.7.1.dist-info}/WHEEL +0 -0
- {cursorflow-2.6.3.dist-info → cursorflow-2.7.1.dist-info}/entry_points.txt +0 -0
- {cursorflow-2.6.3.dist-info → cursorflow-2.7.1.dist-info}/licenses/LICENSE +0 -0
- {cursorflow-2.6.3.dist-info → cursorflow-2.7.1.dist-info}/top_level.txt +0 -0
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
|
297
|
-
|
298
|
-
|
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
|
-
|
309
|
-
|
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',
|
cursorflow/core/cursorflow.py
CHANGED
@@ -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)}")
|