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 +245 -11
- cursorflow/core/browser_controller.py +659 -7
- cursorflow/core/cursor_integration.py +788 -0
- cursorflow/core/cursorflow.py +151 -0
- cursorflow/core/mockup_comparator.py +1316 -0
- cursorflow-1.3.1.dist-info/METADATA +247 -0
- {cursorflow-1.2.0.dist-info → cursorflow-1.3.1.dist-info}/RECORD +11 -9
- cursorflow-1.3.1.dist-info/licenses/LICENSE +21 -0
- cursorflow-1.2.0.dist-info/METADATA +0 -444
- {cursorflow-1.2.0.dist-info → cursorflow-1.3.1.dist-info}/WHEEL +0 -0
- {cursorflow-1.2.0.dist-info → cursorflow-1.3.1.dist-info}/entry_points.txt +0 -0
- {cursorflow-1.2.0.dist-info → cursorflow-1.3.1.dist-info}/top_level.txt +0 -0
cursorflow/cli.py
CHANGED
@@ -26,11 +26,14 @@ def main():
|
|
26
26
|
pass
|
27
27
|
|
28
28
|
@main.command()
|
29
|
-
@click.
|
30
|
-
|
31
|
-
|
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
|
-
|
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
|
-
|
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: {
|
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
|
-
|
114
|
-
|
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]{
|
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')
|