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 +371 -38
- 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.2.dist-info → cursorflow-2.7.1.dist-info}/METADATA +59 -1
- {cursorflow-2.6.2.dist-info → cursorflow-2.7.1.dist-info}/RECORD +12 -9
- {cursorflow-2.6.2.dist-info → cursorflow-2.7.1.dist-info}/WHEEL +0 -0
- {cursorflow-2.6.2.dist-info → cursorflow-2.7.1.dist-info}/entry_points.txt +0 -0
- {cursorflow-2.6.2.dist-info → cursorflow-2.7.1.dist-info}/licenses/LICENSE +0 -0
- {cursorflow-2.6.2.dist-info → cursorflow-2.7.1.dist-info}/top_level.txt +0 -0
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
|
296
|
-
|
297
|
-
|
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
|
-
|
308
|
-
|
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]")
|
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)}")
|