cursorflow 1.2.0__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/updater.py ADDED
@@ -0,0 +1,512 @@
1
+ """
2
+ CursorFlow Update Manager
3
+
4
+ Handles automatic updates, dependency management, and rule synchronization
5
+ across projects. Ensures CursorFlow stays current everywhere it's used.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import subprocess
11
+ import sys
12
+ import urllib.request
13
+ import urllib.error
14
+ from pathlib import Path
15
+ from typing import Dict, List, Optional, Any
16
+ import logging
17
+ import shutil
18
+ import tempfile
19
+ import zipfile
20
+
21
+
22
+ class CursorFlowUpdater:
23
+ """
24
+ Comprehensive update manager for CursorFlow
25
+
26
+ Handles:
27
+ - Package updates via pip
28
+ - Rule synchronization across projects
29
+ - Dependency installation
30
+ - Configuration migration
31
+ - Rollback capabilities
32
+ """
33
+
34
+ def __init__(self, project_dir: str = "."):
35
+ self.project_dir = Path(project_dir).resolve()
36
+ self.logger = logging.getLogger(__name__)
37
+
38
+ # Update configuration
39
+ self.update_config = {
40
+ "check_interval_hours": 24,
41
+ "auto_update": False,
42
+ "include_prereleases": False,
43
+ "backup_before_update": True
44
+ }
45
+
46
+ # Load existing update preferences
47
+ self._load_update_preferences()
48
+
49
+ # GitHub repository info
50
+ self.repo_info = {
51
+ "owner": "haley-marketing-group",
52
+ "repo": "cursorflow",
53
+ "api_base": "https://api.github.com/repos/haley-marketing-group/cursorflow"
54
+ }
55
+
56
+ async def check_for_updates(self, silent: bool = False) -> Dict[str, Any]:
57
+ """
58
+ Check for CursorFlow updates
59
+
60
+ Args:
61
+ silent: If True, don't print status messages
62
+
63
+ Returns:
64
+ Update information dictionary
65
+ """
66
+ if not silent:
67
+ print("🔍 Checking for CursorFlow updates...")
68
+
69
+ try:
70
+ # Get current version
71
+ current_version = self._get_current_version()
72
+
73
+ # Get latest version from PyPI
74
+ latest_version = await self._get_latest_pypi_version()
75
+
76
+ # Get latest rules version from GitHub
77
+ latest_rules_version = await self._get_latest_rules_version()
78
+ current_rules_version = self._get_current_rules_version()
79
+
80
+ update_info = {
81
+ "current_version": current_version,
82
+ "latest_version": latest_version,
83
+ "version_update_available": self._is_newer_version(latest_version, current_version),
84
+ "current_rules_version": current_rules_version,
85
+ "latest_rules_version": latest_rules_version,
86
+ "rules_update_available": self._is_newer_version(latest_rules_version, current_rules_version),
87
+ "dependencies_current": await self._check_dependencies(),
88
+ "last_check": self._get_current_timestamp()
89
+ }
90
+
91
+ # Save last check info
92
+ self._save_update_info(update_info)
93
+
94
+ if not silent:
95
+ self._display_update_status(update_info)
96
+
97
+ return update_info
98
+
99
+ except Exception as e:
100
+ self.logger.error(f"Update check failed: {e}")
101
+ return {"error": str(e), "last_check": self._get_current_timestamp()}
102
+
103
+ async def update_cursorflow(self, force: bool = False) -> bool:
104
+ """
105
+ Update CursorFlow package and rules
106
+
107
+ Args:
108
+ force: Force update even if no updates available
109
+
110
+ Returns:
111
+ True if update successful, False otherwise
112
+ """
113
+ print("🚀 Starting CursorFlow update process...")
114
+
115
+ try:
116
+ # Check what needs updating
117
+ update_info = await self.check_for_updates(silent=True)
118
+
119
+ if not force and not (update_info.get("version_update_available") or
120
+ update_info.get("rules_update_available")):
121
+ print("✅ CursorFlow is already up to date!")
122
+ return True
123
+
124
+ # Create backup if enabled
125
+ if self.update_config.get("backup_before_update", True):
126
+ backup_path = await self._create_backup()
127
+ print(f"📦 Backup created: {backup_path}")
128
+
129
+ success = True
130
+
131
+ # Update package if needed
132
+ if force or update_info.get("version_update_available"):
133
+ print("📥 Updating CursorFlow package...")
134
+ if await self._update_package():
135
+ print("✅ Package updated successfully")
136
+ else:
137
+ print("❌ Package update failed")
138
+ success = False
139
+
140
+ # Update rules if needed
141
+ if force or update_info.get("rules_update_available"):
142
+ print("📝 Updating CursorFlow rules...")
143
+ if await self._update_rules():
144
+ print("✅ Rules updated successfully")
145
+ else:
146
+ print("❌ Rules update failed")
147
+ success = False
148
+
149
+ # Update dependencies
150
+ print("🔧 Checking dependencies...")
151
+ if await self._update_dependencies():
152
+ print("✅ Dependencies updated")
153
+ else:
154
+ print("⚠️ Some dependency updates failed")
155
+
156
+ # Update configuration if needed
157
+ if await self._migrate_configuration():
158
+ print("✅ Configuration updated")
159
+
160
+ # Verify installation
161
+ if await self._verify_installation():
162
+ print("🎉 CursorFlow update completed successfully!")
163
+ return success
164
+ else:
165
+ print("❌ Update verification failed")
166
+ return False
167
+
168
+ except Exception as e:
169
+ self.logger.error(f"Update failed: {e}")
170
+ print(f"❌ Update failed: {e}")
171
+ return False
172
+
173
+ async def install_dependencies(self) -> bool:
174
+ """Install all required dependencies"""
175
+ print("🔧 Installing CursorFlow dependencies...")
176
+
177
+ dependencies = [
178
+ "playwright>=1.40.0",
179
+ "paramiko>=3.0.0",
180
+ "watchdog>=3.0.0",
181
+ "click>=8.0.0",
182
+ "rich>=13.0.0"
183
+ ]
184
+
185
+ try:
186
+ # Install Python dependencies
187
+ for dep in dependencies:
188
+ print(f" Installing {dep}...")
189
+ result = subprocess.run([
190
+ sys.executable, "-m", "pip", "install", dep
191
+ ], capture_output=True, text=True)
192
+
193
+ if result.returncode != 0:
194
+ print(f" ❌ Failed to install {dep}: {result.stderr}")
195
+ return False
196
+ else:
197
+ print(f" ✅ {dep} installed")
198
+
199
+ # Install Playwright browsers
200
+ print(" Installing Playwright browsers...")
201
+ result = subprocess.run([
202
+ sys.executable, "-m", "playwright", "install", "chromium"
203
+ ], capture_output=True, text=True)
204
+
205
+ if result.returncode != 0:
206
+ print(f" ⚠️ Playwright browser install failed: {result.stderr}")
207
+ print(" You may need to run: playwright install chromium")
208
+ else:
209
+ print(" ✅ Playwright browsers installed")
210
+
211
+ return True
212
+
213
+ except Exception as e:
214
+ print(f"❌ Dependency installation failed: {e}")
215
+ return False
216
+
217
+ async def update_project_rules(self, force: bool = False) -> bool:
218
+ """Update rules in current project"""
219
+ print("📝 Updating project rules...")
220
+
221
+ try:
222
+ # Download latest rules
223
+ rules_dir = self.project_dir / ".cursor" / "rules"
224
+ rules_dir.mkdir(parents=True, exist_ok=True)
225
+
226
+ rules_to_update = [
227
+ "cursorflow-usage.mdc",
228
+ "cursorflow-installation.mdc"
229
+ ]
230
+
231
+ for rule_file in rules_to_update:
232
+ print(f" Updating {rule_file}...")
233
+
234
+ # Download from GitHub
235
+ url = f"https://raw.githubusercontent.com/haley-marketing-group/cursorflow/main/rules/{rule_file}"
236
+
237
+ try:
238
+ with urllib.request.urlopen(url) as response:
239
+ content = response.read().decode('utf-8')
240
+
241
+ # Write to project
242
+ rule_path = rules_dir / rule_file
243
+ with open(rule_path, 'w') as f:
244
+ f.write(content)
245
+
246
+ print(f" ✅ {rule_file} updated")
247
+
248
+ except urllib.error.URLError as e:
249
+ print(f" ❌ Failed to download {rule_file}: {e}")
250
+ return False
251
+
252
+ # Update rules version tracking
253
+ self._save_rules_version()
254
+
255
+ return True
256
+
257
+ except Exception as e:
258
+ print(f"❌ Rules update failed: {e}")
259
+ return False
260
+
261
+ def _get_current_version(self) -> str:
262
+ """Get currently installed CursorFlow version"""
263
+ try:
264
+ import cursorflow
265
+ return getattr(cursorflow, '__version__', '0.0.0')
266
+ except ImportError:
267
+ return '0.0.0'
268
+
269
+ async def _get_latest_pypi_version(self) -> str:
270
+ """Get latest version from PyPI"""
271
+ try:
272
+ url = "https://pypi.org/pypi/cursorflow/json"
273
+ with urllib.request.urlopen(url) as response:
274
+ data = json.loads(response.read())
275
+ return data["info"]["version"]
276
+ except Exception:
277
+ return "0.0.0"
278
+
279
+ async def _get_latest_rules_version(self) -> str:
280
+ """Get latest rules version from GitHub"""
281
+ try:
282
+ url = f"{self.repo_info['api_base']}/releases/latest"
283
+ with urllib.request.urlopen(url) as response:
284
+ data = json.loads(response.read())
285
+ return data["tag_name"].lstrip('v')
286
+ except Exception:
287
+ return "0.0.0"
288
+
289
+ def _get_current_rules_version(self) -> str:
290
+ """Get current rules version in project"""
291
+ version_file = self.project_dir / ".cursorflow" / "rules_version.json"
292
+ if version_file.exists():
293
+ try:
294
+ with open(version_file) as f:
295
+ data = json.load(f)
296
+ return data.get("version", "0.0.0")
297
+ except Exception:
298
+ pass
299
+ return "0.0.0"
300
+
301
+ def _is_newer_version(self, latest: str, current: str) -> bool:
302
+ """Compare version strings"""
303
+ try:
304
+ latest_parts = [int(x) for x in latest.split('.')]
305
+ current_parts = [int(x) for x in current.split('.')]
306
+
307
+ # Pad shorter version with zeros
308
+ max_len = max(len(latest_parts), len(current_parts))
309
+ latest_parts.extend([0] * (max_len - len(latest_parts)))
310
+ current_parts.extend([0] * (max_len - len(current_parts)))
311
+
312
+ return latest_parts > current_parts
313
+ except Exception:
314
+ return False
315
+
316
+ async def _check_dependencies(self) -> bool:
317
+ """Check if all dependencies are current"""
318
+ try:
319
+ import playwright
320
+ import paramiko
321
+ import watchdog
322
+ return True
323
+ except ImportError:
324
+ return False
325
+
326
+ async def _update_package(self) -> bool:
327
+ """Update CursorFlow package via pip"""
328
+ try:
329
+ result = subprocess.run([
330
+ sys.executable, "-m", "pip", "install", "--upgrade", "cursorflow"
331
+ ], capture_output=True, text=True)
332
+
333
+ return result.returncode == 0
334
+ except Exception:
335
+ return False
336
+
337
+ async def _update_rules(self) -> bool:
338
+ """Update rules from GitHub"""
339
+ return await self.update_project_rules()
340
+
341
+ async def _update_dependencies(self) -> bool:
342
+ """Update all dependencies"""
343
+ return await self.install_dependencies()
344
+
345
+ async def _migrate_configuration(self) -> bool:
346
+ """Migrate configuration to new format if needed"""
347
+ config_file = self.project_dir / "cursorflow-config.json"
348
+
349
+ if not config_file.exists():
350
+ return True
351
+
352
+ try:
353
+ with open(config_file) as f:
354
+ config = json.load(f)
355
+
356
+ # Add new fields if missing
357
+ updated = False
358
+
359
+ if "_cursorflow_version" not in config:
360
+ config["_cursorflow_version"] = self._get_current_version()
361
+ updated = True
362
+
363
+ if "browser" not in config:
364
+ config["browser"] = {"headless": True, "debug_mode": False}
365
+ updated = True
366
+
367
+ if updated:
368
+ with open(config_file, 'w') as f:
369
+ json.dump(config, f, indent=2)
370
+
371
+ return True
372
+
373
+ except Exception as e:
374
+ self.logger.error(f"Configuration migration failed: {e}")
375
+ return False
376
+
377
+ async def _verify_installation(self) -> bool:
378
+ """Verify that CursorFlow is working after update"""
379
+ try:
380
+ # Try importing
381
+ import cursorflow
382
+
383
+ # Check core components
384
+ from cursorflow import CursorFlow
385
+ from cursorflow.core.css_iterator import CSSIterator
386
+
387
+ return True
388
+ except Exception:
389
+ return False
390
+
391
+ async def _create_backup(self) -> str:
392
+ """Create backup of current installation"""
393
+ backup_dir = self.project_dir / ".cursorflow" / "backups"
394
+ backup_dir.mkdir(parents=True, exist_ok=True)
395
+
396
+ timestamp = self._get_current_timestamp()
397
+ backup_name = f"backup_{timestamp}"
398
+ backup_path = backup_dir / backup_name
399
+
400
+ # Backup configuration and rules
401
+ if (self.project_dir / "cursorflow-config.json").exists():
402
+ shutil.copy2(
403
+ self.project_dir / "cursorflow-config.json",
404
+ backup_path.with_suffix('.config.json')
405
+ )
406
+
407
+ rules_dir = self.project_dir / ".cursor" / "rules"
408
+ if rules_dir.exists():
409
+ shutil.copytree(rules_dir, backup_path.with_suffix('.rules'), dirs_exist_ok=True)
410
+
411
+ return str(backup_path)
412
+
413
+ def _load_update_preferences(self):
414
+ """Load update preferences from project"""
415
+ prefs_file = self.project_dir / ".cursorflow" / "update_preferences.json"
416
+ if prefs_file.exists():
417
+ try:
418
+ with open(prefs_file) as f:
419
+ prefs = json.load(f)
420
+ self.update_config.update(prefs)
421
+ except Exception:
422
+ pass
423
+
424
+ def _save_update_info(self, update_info: Dict):
425
+ """Save update check information"""
426
+ info_dir = self.project_dir / ".cursorflow"
427
+ info_dir.mkdir(exist_ok=True)
428
+
429
+ info_file = info_dir / "update_info.json"
430
+ with open(info_file, 'w') as f:
431
+ json.dump(update_info, f, indent=2)
432
+
433
+ def _save_rules_version(self):
434
+ """Save current rules version"""
435
+ version_dir = self.project_dir / ".cursorflow"
436
+ version_dir.mkdir(exist_ok=True)
437
+
438
+ version_file = version_dir / "rules_version.json"
439
+ with open(version_file, 'w') as f:
440
+ json.dump({
441
+ "version": self._get_current_version(),
442
+ "updated": self._get_current_timestamp()
443
+ }, f, indent=2)
444
+
445
+ def _get_current_timestamp(self) -> str:
446
+ """Get current timestamp string"""
447
+ import datetime
448
+ return datetime.datetime.now().isoformat()
449
+
450
+ def _display_update_status(self, update_info: Dict):
451
+ """Display update status to user"""
452
+ current_version = update_info.get("current_version", "unknown")
453
+ latest_version = update_info.get("latest_version", "unknown")
454
+
455
+ print(f"📦 Current version: {current_version}")
456
+ print(f"🌟 Latest version: {latest_version}")
457
+
458
+ if update_info.get("version_update_available"):
459
+ print("🔄 Package update available!")
460
+
461
+ if update_info.get("rules_update_available"):
462
+ print("📝 Rules update available!")
463
+
464
+ if not update_info.get("dependencies_current"):
465
+ print("🔧 Dependency updates needed!")
466
+
467
+ if not (update_info.get("version_update_available") or
468
+ update_info.get("rules_update_available")):
469
+ print("✅ CursorFlow is up to date!")
470
+
471
+
472
+ async def check_updates(project_dir: str = ".") -> Dict[str, Any]:
473
+ """Convenience function to check for updates"""
474
+ updater = CursorFlowUpdater(project_dir)
475
+ return await updater.check_for_updates()
476
+
477
+
478
+ async def update_cursorflow(project_dir: str = ".", force: bool = False) -> bool:
479
+ """Convenience function to update CursorFlow"""
480
+ updater = CursorFlowUpdater(project_dir)
481
+ return await updater.update_cursorflow(force=force)
482
+
483
+
484
+ async def install_dependencies(project_dir: str = ".") -> bool:
485
+ """Convenience function to install dependencies"""
486
+ updater = CursorFlowUpdater(project_dir)
487
+ return await updater.install_dependencies()
488
+
489
+
490
+ if __name__ == "__main__":
491
+ import argparse
492
+
493
+ parser = argparse.ArgumentParser(description="CursorFlow Update Manager")
494
+ parser.add_argument("--check", action="store_true", help="Check for updates")
495
+ parser.add_argument("--update", action="store_true", help="Update CursorFlow")
496
+ parser.add_argument("--install-deps", action="store_true", help="Install dependencies")
497
+ parser.add_argument("--force", action="store_true", help="Force update")
498
+ parser.add_argument("--project-dir", default=".", help="Project directory")
499
+
500
+ args = parser.parse_args()
501
+
502
+ if args.check:
503
+ result = asyncio.run(check_updates(args.project_dir))
504
+ print(json.dumps(result, indent=2))
505
+ elif args.update:
506
+ success = asyncio.run(update_cursorflow(args.project_dir, force=args.force))
507
+ sys.exit(0 if success else 1)
508
+ elif args.install_deps:
509
+ success = asyncio.run(install_dependencies(args.project_dir))
510
+ sys.exit(0 if success else 1)
511
+ else:
512
+ parser.print_help()