cursorflow 1.2.0__py3-none-any.whl → 1.3.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
@@ -26,11 +26,14 @@ def main():
26
26
  pass
27
27
 
28
28
  @main.command()
29
- @click.argument('test_name', required=False, default='ui-test')
30
- @click.option('--base-url', '-u', default='http://localhost:3000',
31
- help='Base URL for testing')
29
+ @click.option('--base-url', '-u', required=True,
30
+ help='Base URL for testing (e.g., http://localhost:3000)')
31
+ @click.option('--path', '-p',
32
+ help='Simple path to navigate to (e.g., "/dashboard")')
32
33
  @click.option('--actions', '-a',
33
34
  help='JSON file with test actions, or inline JSON string')
35
+ @click.option('--output', '-o',
36
+ help='Output file for results (auto-generated if not specified)')
34
37
  @click.option('--logs', '-l',
35
38
  type=click.Choice(['local', 'ssh', 'docker', 'systemd']),
36
39
  default='local',
@@ -39,7 +42,11 @@ def main():
39
42
  help='Configuration file path')
40
43
  @click.option('--verbose', '-v', is_flag=True,
41
44
  help='Verbose output')
42
- def test(test_name, base_url, actions, logs, config, verbose):
45
+ @click.option('--headless', is_flag=True, default=True,
46
+ help='Run browser in headless mode')
47
+ @click.option('--timeout', type=int, default=30,
48
+ help='Timeout in seconds for actions')
49
+ def test(base_url, path, actions, output, logs, config, verbose, headless, timeout):
43
50
  """Test UI flows and interactions with real-time log monitoring"""
44
51
 
45
52
  if verbose:
@@ -65,14 +72,22 @@ def test(test_name, base_url, actions, logs, config, verbose):
65
72
  except Exception as e:
66
73
  console.print(f"[red]❌ Failed to load actions: {e}[/red]")
67
74
  return
75
+ elif path:
76
+ # Simple path navigation
77
+ test_actions = [
78
+ {"navigate": path},
79
+ {"wait_for": "body"},
80
+ {"screenshot": "page_loaded"}
81
+ ]
82
+ console.print(f"📋 Using simple path navigation to [cyan]{path}[/cyan]")
68
83
  else:
69
- # Default actions - just navigate and screenshot
84
+ # Default actions - just navigate to root and screenshot
70
85
  test_actions = [
71
86
  {"navigate": "/"},
72
87
  {"wait_for": "body"},
73
88
  {"screenshot": "baseline"}
74
89
  ]
75
- console.print(f"📋 Using default actions (navigate + screenshot)")
90
+ console.print(f"📋 Using default actions (navigate to root + screenshot)")
76
91
 
77
92
  # Load configuration
78
93
  agent_config = {}
@@ -80,7 +95,8 @@ def test(test_name, base_url, actions, logs, config, verbose):
80
95
  with open(config, 'r') as f:
81
96
  agent_config = json.load(f)
82
97
 
83
- console.print(f"🎯 Testing [bold]{test_name}[/bold] at [blue]{base_url}[/blue]")
98
+ test_description = path if path else "root page"
99
+ console.print(f"🎯 Testing [bold]{test_description}[/bold] at [blue]{base_url}[/blue]")
84
100
 
85
101
  # Initialize CursorFlow (framework-agnostic)
86
102
  try:
@@ -88,6 +104,7 @@ def test(test_name, base_url, actions, logs, config, verbose):
88
104
  flow = CursorFlow(
89
105
  base_url=base_url,
90
106
  log_config={'source': logs, 'paths': ['logs/app.log']},
107
+ browser_config={'headless': headless, 'timeout': timeout},
91
108
  **agent_config
92
109
  )
93
110
  except Exception as e:
@@ -99,7 +116,7 @@ def test(test_name, base_url, actions, logs, config, verbose):
99
116
  console.print(f"🚀 Executing {len(test_actions)} actions...")
100
117
  results = asyncio.run(flow.execute_and_collect(test_actions))
101
118
 
102
- console.print(f"✅ Test completed: {test_name}")
119
+ console.print(f"✅ Test completed: {test_description}")
103
120
  console.print(f"📊 Browser events: {len(results.get('browser_events', []))}")
104
121
  console.print(f"📋 Server logs: {len(results.get('server_logs', []))}")
105
122
  console.print(f"📸 Screenshots: {len(results.get('artifacts', {}).get('screenshots', []))}")
@@ -110,11 +127,16 @@ def test(test_name, base_url, actions, logs, config, verbose):
110
127
  console.print(f"⏰ Timeline events: {len(timeline)}")
111
128
 
112
129
  # Save results to file for Cursor analysis
113
- output_file = f"{test_name.replace(' ', '_')}_test_results.json"
114
- with open(output_file, 'w') as f:
130
+ if not output:
131
+ # Auto-generate meaningful filename
132
+ session_id = results.get('session_id', 'unknown')
133
+ path_part = path.replace('/', '_') if path else 'root'
134
+ output = f"cursorflow_{path_part}_{session_id}.json"
135
+
136
+ with open(output, 'w') as f:
115
137
  json.dump(results, f, indent=2, default=str)
116
138
 
117
- console.print(f"💾 Full results saved to: [cyan]{output_file}[/cyan]")
139
+ console.print(f"💾 Full results saved to: [cyan]{output}[/cyan]")
118
140
  console.print(f"📁 Artifacts stored in: [cyan].cursorflow/artifacts/[/cyan]")
119
141
 
120
142
  except Exception as e:
@@ -124,6 +146,218 @@ def test(test_name, base_url, actions, logs, config, verbose):
124
146
  console.print(traceback.format_exc())
125
147
  raise
126
148
 
149
+ @main.command()
150
+ @click.argument('mockup_url', required=True)
151
+ @click.option('--base-url', '-u', default='http://localhost:3000',
152
+ help='Base URL of work-in-progress implementation')
153
+ @click.option('--mockup-actions', '-ma',
154
+ help='JSON file with actions to perform on mockup, or inline JSON string')
155
+ @click.option('--implementation-actions', '-ia',
156
+ help='JSON file with actions to perform on implementation, or inline JSON string')
157
+ @click.option('--viewports', '-v',
158
+ help='JSON array of viewports to test: [{"width": 1440, "height": 900, "name": "desktop"}]')
159
+ @click.option('--diff-threshold', '-t', type=float, default=0.1,
160
+ help='Visual difference threshold (0.0-1.0)')
161
+ @click.option('--output', '-o', default='mockup_comparison_results.json',
162
+ help='Output file for comparison results')
163
+ @click.option('--verbose', is_flag=True,
164
+ help='Verbose output')
165
+ def compare_mockup(mockup_url, base_url, mockup_actions, implementation_actions, viewports, diff_threshold, output, verbose):
166
+ """Compare mockup design to work-in-progress implementation"""
167
+
168
+ console.print(f"🎨 Comparing mockup [blue]{mockup_url}[/blue] to implementation [blue]{base_url}[/blue]")
169
+
170
+ # Parse actions
171
+ def parse_actions(actions_input):
172
+ if not actions_input:
173
+ return None
174
+
175
+ if actions_input.startswith('[') or actions_input.startswith('{'):
176
+ return json.loads(actions_input)
177
+ else:
178
+ with open(actions_input, 'r') as f:
179
+ return json.load(f)
180
+
181
+ try:
182
+ mockup_actions_parsed = parse_actions(mockup_actions)
183
+ implementation_actions_parsed = parse_actions(implementation_actions)
184
+
185
+ # Parse viewports
186
+ viewports_parsed = None
187
+ if viewports:
188
+ if viewports.startswith('['):
189
+ viewports_parsed = json.loads(viewports)
190
+ else:
191
+ with open(viewports, 'r') as f:
192
+ viewports_parsed = json.load(f)
193
+
194
+ # Build comparison config
195
+ comparison_config = {
196
+ "diff_threshold": diff_threshold
197
+ }
198
+ if viewports_parsed:
199
+ comparison_config["viewports"] = viewports_parsed
200
+
201
+ except Exception as e:
202
+ console.print(f"[red]Error parsing input parameters: {e}[/red]")
203
+ return
204
+
205
+ # Initialize CursorFlow
206
+ try:
207
+ from .core.cursorflow import CursorFlow
208
+ flow = CursorFlow(
209
+ base_url=base_url,
210
+ log_config={'source': 'local', 'paths': ['logs/app.log']},
211
+ browser_config={'headless': True}
212
+ )
213
+ except Exception as e:
214
+ console.print(f"[red]Error initializing CursorFlow: {e}[/red]")
215
+ return
216
+
217
+ # Execute mockup comparison
218
+ try:
219
+ console.print("🚀 Starting mockup comparison...")
220
+ results = asyncio.run(flow.compare_mockup_to_implementation(
221
+ mockup_url=mockup_url,
222
+ mockup_actions=mockup_actions_parsed,
223
+ implementation_actions=implementation_actions_parsed,
224
+ comparison_config=comparison_config
225
+ ))
226
+
227
+ if "error" in results:
228
+ console.print(f"[red]❌ Comparison failed: {results['error']}[/red]")
229
+ return
230
+
231
+ # Display results summary
232
+ summary = results.get('summary', {})
233
+ console.print(f"✅ Comparison completed: {results.get('comparison_id', 'unknown')}")
234
+ console.print(f"📊 Average similarity: [bold]{summary.get('average_similarity', 0)}%[/bold]")
235
+ console.print(f"📱 Viewports tested: {summary.get('viewports_tested', 0)}")
236
+
237
+ # Show recommendations
238
+ recommendations = results.get('recommendations', [])
239
+ if recommendations:
240
+ console.print(f"💡 Recommendations: {len(recommendations)} improvements suggested")
241
+ for i, rec in enumerate(recommendations[:3]): # Show first 3
242
+ console.print(f" {i+1}. {rec.get('description', 'No description')}")
243
+
244
+ # Save results
245
+ with open(output, 'w') as f:
246
+ json.dump(results, f, indent=2, default=str)
247
+
248
+ console.print(f"💾 Full results saved to: [cyan]{output}[/cyan]")
249
+ console.print(f"📁 Visual diffs stored in: [cyan].cursorflow/artifacts/[/cyan]")
250
+
251
+ except Exception as e:
252
+ console.print(f"[red]❌ Comparison failed: {e}[/red]")
253
+ if verbose:
254
+ import traceback
255
+ console.print(traceback.format_exc())
256
+ raise
257
+
258
+ @main.command()
259
+ @click.argument('mockup_url', required=True)
260
+ @click.option('--base-url', '-u', default='http://localhost:3000',
261
+ help='Base URL of work-in-progress implementation')
262
+ @click.option('--css-improvements', '-c', required=True,
263
+ help='JSON file with CSS improvements to test, or inline JSON string')
264
+ @click.option('--base-actions', '-a',
265
+ help='JSON file with base actions to perform before each test')
266
+ @click.option('--diff-threshold', '-t', type=float, default=0.1,
267
+ help='Visual difference threshold (0.0-1.0)')
268
+ @click.option('--output', '-o', default='mockup_iteration_results.json',
269
+ help='Output file for iteration results')
270
+ @click.option('--verbose', is_flag=True,
271
+ help='Verbose output')
272
+ def iterate_mockup(mockup_url, base_url, css_improvements, base_actions, diff_threshold, output, verbose):
273
+ """Iteratively improve implementation to match mockup design"""
274
+
275
+ console.print(f"🔄 Iterating on [blue]{base_url}[/blue] to match [blue]{mockup_url}[/blue]")
276
+
277
+ # Parse CSS improvements
278
+ def parse_json_input(input_str):
279
+ if not input_str:
280
+ return None
281
+
282
+ if input_str.startswith('[') or input_str.startswith('{'):
283
+ return json.loads(input_str)
284
+ else:
285
+ with open(input_str, 'r') as f:
286
+ return json.load(f)
287
+
288
+ try:
289
+ css_improvements_parsed = parse_json_input(css_improvements)
290
+ base_actions_parsed = parse_json_input(base_actions)
291
+
292
+ if not css_improvements_parsed:
293
+ console.print("[red]Error: CSS improvements are required[/red]")
294
+ return
295
+
296
+ comparison_config = {"diff_threshold": diff_threshold}
297
+
298
+ except Exception as e:
299
+ console.print(f"[red]Error parsing input parameters: {e}[/red]")
300
+ return
301
+
302
+ # Initialize CursorFlow
303
+ try:
304
+ from .core.cursorflow import CursorFlow
305
+ flow = CursorFlow(
306
+ base_url=base_url,
307
+ log_config={'source': 'local', 'paths': ['logs/app.log']},
308
+ browser_config={'headless': True}
309
+ )
310
+ except Exception as e:
311
+ console.print(f"[red]Error initializing CursorFlow: {e}[/red]")
312
+ return
313
+
314
+ # Execute iterative mockup matching
315
+ try:
316
+ console.print(f"🚀 Starting iterative matching with {len(css_improvements_parsed)} CSS improvements...")
317
+ results = asyncio.run(flow.iterative_mockup_matching(
318
+ mockup_url=mockup_url,
319
+ css_improvements=css_improvements_parsed,
320
+ base_actions=base_actions_parsed,
321
+ comparison_config=comparison_config
322
+ ))
323
+
324
+ if "error" in results:
325
+ console.print(f"[red]❌ Iteration failed: {results['error']}[/red]")
326
+ return
327
+
328
+ # Display results summary
329
+ summary = results.get('summary', {})
330
+ console.print(f"✅ Iteration completed: {results.get('session_id', 'unknown')}")
331
+ console.print(f"📊 Total improvement: [bold]{summary.get('total_improvement', 0)}%[/bold]")
332
+ console.print(f"🔄 Successful iterations: {summary.get('successful_iterations', 0)}/{summary.get('total_iterations', 0)}")
333
+
334
+ # Show best iteration
335
+ best_iteration = results.get('best_iteration')
336
+ if best_iteration:
337
+ console.print(f"🏆 Best iteration: {best_iteration.get('css_change', {}).get('name', 'unnamed')}")
338
+ console.print(f" Similarity achieved: {best_iteration.get('similarity_achieved', 0)}%")
339
+
340
+ # Show final recommendations
341
+ recommendations = results.get('final_recommendations', [])
342
+ if recommendations:
343
+ console.print(f"💡 Final recommendations: {len(recommendations)} actions suggested")
344
+ for i, rec in enumerate(recommendations[:3]):
345
+ console.print(f" {i+1}. {rec.get('description', 'No description')}")
346
+
347
+ # Save results
348
+ with open(output, 'w') as f:
349
+ json.dump(results, f, indent=2, default=str)
350
+
351
+ console.print(f"💾 Full results saved to: [cyan]{output}[/cyan]")
352
+ console.print(f"📁 Iteration progress stored in: [cyan].cursorflow/artifacts/[/cyan]")
353
+
354
+ except Exception as e:
355
+ console.print(f"[red]❌ Iteration failed: {e}[/red]")
356
+ if verbose:
357
+ import traceback
358
+ console.print(traceback.format_exc())
359
+ raise
360
+
127
361
  @main.command()
128
362
  @click.option('--project-path', '-p', default='.',
129
363
  help='Project directory path')