.
");',
+ # Token limit bypass
+ r'async getEffectiveTokenLimit(e){const n=e.modelName;if(!n)return 2e5;':
+ r'async getEffectiveTokenLimit(e){return 9000000;const n=e.modelName;if(!n)return 9e5;',
- # Toast ๆฟๆข
+ # Pro status
+ r'var DWr=ne("
You are currently signed in with
.");':
+ r'var DWr=ne("
You are currently signed in with .
Pro
");',
+
+ # Toast replacement
r'notifications-toasts': r'notifications-toasts hidden'
}
- # ไฝฟ็จpatterns่ฟ่กๆฟๆข
+ # Apply replacements
for old_pattern, new_pattern in patterns.items():
content = content.replace(old_pattern, new_pattern)
@@ -158,24 +217,40 @@ def modify_workbench_js(file_path: str, translator=None) -> bool:
# Backup original file with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = f"{file_path}.backup.{timestamp}"
- shutil.copy2(file_path, backup_path)
- print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('reset.backup_created', path=backup_path)}{Style.RESET_ALL}")
+ try:
+ shutil.copy2(file_path, backup_path)
+ logger.info(f"Backup created: {backup_path}")
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('reset.backup_created', path=backup_path) if translator else f'Backup created: {backup_path}'}{Style.RESET_ALL}")
+ except Exception as e:
+ logger.error(f"Failed to create backup: {e}")
+ os.unlink(tmp_path)
+ return False
# Move temporary file to original position
- if os.path.exists(file_path):
- os.remove(file_path)
- shutil.move(tmp_path, file_path)
+ try:
+ if os.path.exists(file_path):
+ os.remove(file_path)
+ shutil.move(tmp_path, file_path)
+ except Exception as e:
+ logger.error(f"Failed to replace original file: {e}")
+ return False
# Restore original permissions
- os.chmod(file_path, original_mode)
- if os.name != "nt": # Not Windows
- os.chown(file_path, original_uid, original_gid)
+ try:
+ os.chmod(file_path, original_mode)
+ if os.name != "nt": # Not Windows
+ os.chown(file_path, original_uid, original_gid)
+ except Exception as e:
+ logger.warning(f"Failed to restore original permissions: {e}")
+ # Continue anyway as this is not critical
- print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('reset.file_modified')}{Style.RESET_ALL}")
+ logger.info("File modified successfully")
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('reset.file_modified') if translator else 'File modified successfully'}{Style.RESET_ALL}")
return True
except Exception as e:
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('reset.modify_file_failed', error=str(e))}{Style.RESET_ALL}")
+ logger.error(f"Failed to modify file: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('reset.modify_file_failed', error=str(e)) if translator else f'Failed to modify file: {str(e)}'}{Style.RESET_ALL}")
if "tmp_path" in locals():
try:
os.unlink(tmp_path)
@@ -183,19 +258,53 @@ def modify_workbench_js(file_path: str, translator=None) -> bool:
pass
return False
-def run(translator=None):
- config = get_config(translator)
- if not config:
+def run(translator: Any = None) -> bool:
+ """Run the token limit bypass
+
+ Args:
+ translator: Optional translator for internationalization
+
+ Returns:
+ bool: True if successful, False otherwise
+ """
+ try:
+ config = get_config(translator)
+ if not config:
+ logger.error("Failed to get configuration")
+ return False
+
+ print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
+ print(f"{Fore.CYAN}{EMOJI['RESET']} {translator.get('bypass_token_limit.title') if translator else 'Bypass Token Limit'}{Style.RESET_ALL}")
+ print(f"{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
+
+ # Get workbench.desktop.main.js path
+ try:
+ workbench_path = get_workbench_cursor_path(translator)
+ logger.info(f"Found workbench.desktop.main.js at: {workbench_path}")
+ except OSError as e:
+ logger.error(f"Failed to get workbench path: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {str(e)}{Style.RESET_ALL}")
+ return False
+
+ # Modify the file
+ success = modify_workbench_js(workbench_path, translator)
+
+ print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
+ input(f"{EMOJI['INFO']} {translator.get('bypass_token_limit.press_enter') if translator else 'Press Enter to continue...'}")
+
+ return success
+ except Exception as e:
+ logger.error(f"Error in run function: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} An unexpected error occurred: {str(e)}{Style.RESET_ALL}")
return False
- print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
- print(f"{Fore.CYAN}{EMOJI['RESET']} {translator.get('bypass_token_limit.title')}{Style.RESET_ALL}")
- print(f"{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
-
- modify_workbench_js(get_workbench_cursor_path(translator), translator)
-
- print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
- input(f"{EMOJI['INFO']} {translator.get('bypass_token_limit.press_enter')}...")
if __name__ == "__main__":
- from main import translator as main_translator
- run(main_translator)
\ No newline at end of file
+ try:
+ from main import translator as main_translator
+ run(main_translator)
+ except ImportError:
+ logger.warning("Failed to import translator from main.py, running without translation")
+ run(None)
+ except Exception as e:
+ logger.error(f"Error running bypass_token_limit.py: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} An unexpected error occurred: {str(e)}{Style.RESET_ALL}")
\ No newline at end of file
diff --git a/bypass_version.py b/bypass_version.py
index bde0c73..3b27f89 100644
--- a/bypass_version.py
+++ b/bypass_version.py
@@ -4,13 +4,24 @@ import shutil
import platform
import configparser
import time
+import logging
from colorama import Fore, Style, init
import sys
import traceback
+from pathlib import Path
+from typing import Optional, Dict, List, Union, Tuple, Any
from utils import get_user_documents_path
# Initialize colorama
-init()
+init(autoreset=True)
+
+# Configure logging
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S"
+)
+logger = logging.getLogger(__name__)
# Define emoji constants
EMOJI = {
@@ -24,8 +35,18 @@ EMOJI = {
'VERSION': '๐ท๏ธ'
}
-def get_product_json_path(translator=None):
- """Get Cursor product.json path"""
+def get_product_json_path(translator: Any = None) -> str:
+ """Get Cursor product.json path based on the operating system.
+
+ Args:
+ translator: Optional translator for internationalization
+
+ Returns:
+ str: Path to the product.json file
+
+ Raises:
+ OSError: If the file is not found or the OS is not supported
+ """
system = platform.system()
# Read configuration
@@ -36,10 +57,13 @@ def get_product_json_path(translator=None):
if os.path.exists(config_file):
config.read(config_file)
+ # Define paths for different operating systems
if system == "Windows":
localappdata = os.environ.get("LOCALAPPDATA")
if not localappdata:
- raise OSError(translator.get('bypass.localappdata_not_found') if translator else "LOCALAPPDATA environment variable not found")
+ error_msg = "LOCALAPPDATA environment variable not found"
+ logger.error(error_msg)
+ raise OSError(translator.get('bypass.localappdata_not_found') if translator else error_msg)
product_json_path = os.path.join(localappdata, "Programs", "Cursor", "resources", "app", "product.json")
@@ -66,38 +90,72 @@ def get_product_json_path(translator=None):
if os.path.exists(extracted_usr_paths):
possible_paths.append(extracted_usr_paths)
+ # Find first existing path
for path in possible_paths:
if os.path.exists(path):
product_json_path = path
break
else:
- raise OSError(translator.get('bypass.product_json_not_found') if translator else "product.json not found in common Linux paths")
+ error_msg = "product.json not found in common Linux paths"
+ logger.error(error_msg)
+ raise OSError(translator.get('bypass.product_json_not_found') if translator else error_msg)
else:
- raise OSError(translator.get('bypass.unsupported_os', system=system) if translator else f"Unsupported operating system: {system}")
+ error_msg = f"Unsupported operating system: {system}"
+ logger.error(error_msg)
+ raise OSError(translator.get('bypass.unsupported_os', system=system) if translator else error_msg)
+ # Verify that the file exists
if not os.path.exists(product_json_path):
- raise OSError(translator.get('bypass.file_not_found', path=product_json_path) if translator else f"File not found: {product_json_path}")
+ error_msg = f"File not found: {product_json_path}"
+ logger.error(error_msg)
+ raise OSError(translator.get('bypass.file_not_found', path=product_json_path) if translator else error_msg)
+ logger.info(f"Found product.json at: {product_json_path}")
return product_json_path
-def compare_versions(version1, version2):
- """Compare two version strings"""
- v1_parts = [int(x) for x in version1.split('.')]
- v2_parts = [int(x) for x in version2.split('.')]
+def compare_versions(version1: str, version2: str) -> int:
+ """Compare two version strings.
- for i in range(max(len(v1_parts), len(v2_parts))):
- v1 = v1_parts[i] if i < len(v1_parts) else 0
- v2 = v2_parts[i] if i < len(v2_parts) else 0
- if v1 < v2:
+ Args:
+ version1: First version string (e.g., "0.48.7")
+ version2: Second version string (e.g., "0.46.0")
+
+ Returns:
+ int: -1 if version1 < version2, 0 if version1 == version2, 1 if version1 > version2
+ """
+ try:
+ v1_parts = [int(x) for x in version1.split('.')]
+ v2_parts = [int(x) for x in version2.split('.')]
+
+ for i in range(max(len(v1_parts), len(v2_parts))):
+ v1 = v1_parts[i] if i < len(v1_parts) else 0
+ v2 = v2_parts[i] if i < len(v2_parts) else 0
+ if v1 < v2:
+ return -1
+ elif v1 > v2:
+ return 1
+
+ return 0
+ except (ValueError, TypeError) as e:
+ logger.warning(f"Error comparing versions {version1} and {version2}: {e}")
+ # Fall back to string comparison if numeric comparison fails
+ if version1 < version2:
return -1
- elif v1 > v2:
+ elif version1 > version2:
return 1
-
- return 0
+ else:
+ return 0
-def bypass_version(translator=None):
- """Bypass Cursor version check by modifying product.json"""
+def bypass_version(translator: Any = None) -> bool:
+ """Bypass Cursor version check by modifying product.json.
+
+ Args:
+ translator: Optional translator for internationalization
+
+ Returns:
+ bool: True if the version was successfully bypassed, False otherwise
+ """
try:
print(f"\n{Fore.CYAN}{EMOJI['INFO']} {translator.get('bypass.starting') if translator else 'Starting Cursor version bypass...'}{Style.RESET_ALL}")
@@ -114,7 +172,12 @@ def bypass_version(translator=None):
try:
with open(product_json_path, "r", encoding="utf-8") as f:
product_data = json.load(f)
+ except json.JSONDecodeError as e:
+ logger.error(f"Invalid JSON in product.json: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('bypass.invalid_json', error=str(e)) if translator else f'Invalid JSON in product.json: {str(e)}'}{Style.RESET_ALL}")
+ return False
except Exception as e:
+ logger.error(f"Failed to read product.json: {e}")
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('bypass.read_failed', error=str(e)) if translator else f'Failed to read product.json: {str(e)}'}{Style.RESET_ALL}")
return False
@@ -122,39 +185,98 @@ def bypass_version(translator=None):
current_version = product_data.get("version", "0.0.0")
print(f"{Fore.CYAN}{EMOJI['VERSION']} {translator.get('bypass.current_version', version=current_version) if translator else f'Current version: {current_version}'}{Style.RESET_ALL}")
+ # Target version to set
+ new_version = "0.48.7"
+
# Check if version needs to be modified
if compare_versions(current_version, "0.46.0") < 0:
# Create backup
timestamp = time.strftime("%Y%m%d%H%M%S")
backup_path = f"{product_json_path}.{timestamp}"
- shutil.copy2(product_json_path, backup_path)
- print(f"{Fore.GREEN}{EMOJI['BACKUP']} {translator.get('bypass.backup_created', path=backup_path) if translator else f'Backup created: {backup_path}'}{Style.RESET_ALL}")
+ try:
+ shutil.copy2(product_json_path, backup_path)
+ print(f"{Fore.GREEN}{EMOJI['BACKUP']} {translator.get('bypass.backup_created', path=backup_path) if translator else f'Backup created: {backup_path}'}{Style.RESET_ALL}")
+ except Exception as e:
+ logger.error(f"Failed to create backup: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('bypass.backup_failed', error=str(e)) if translator else f'Failed to create backup: {str(e)}'}{Style.RESET_ALL}")
+ return False
# Modify version
- new_version = "0.48.7"
product_data["version"] = new_version
# Save modified product.json
try:
with open(product_json_path, "w", encoding="utf-8") as f:
json.dump(product_data, f, indent=2)
+ logger.info(f"Version updated from {current_version} to {new_version}")
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('bypass.version_updated', old=current_version, new=new_version) if translator else f'Version updated from {current_version} to {new_version}'}{Style.RESET_ALL}")
return True
except Exception as e:
+ logger.error(f"Failed to write product.json: {e}")
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('bypass.write_failed', error=str(e)) if translator else f'Failed to write product.json: {str(e)}'}{Style.RESET_ALL}")
+
+ # Try to restore from backup if write fails
+ try:
+ if os.path.exists(backup_path):
+ shutil.copy2(backup_path, product_json_path)
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('bypass.restored_from_backup') if translator else 'Restored from backup'}{Style.RESET_ALL}")
+ except Exception as restore_error:
+ logger.error(f"Failed to restore from backup: {restore_error}")
+
return False
else:
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('bypass.no_update_needed', version=current_version) if translator else f'No update needed. Current version {current_version} is already >= 0.46.0'}{Style.RESET_ALL}")
return True
except Exception as e:
+ logger.error(f"Version bypass failed: {e}")
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('bypass.bypass_failed', error=str(e)) if translator else f'Version bypass failed: {str(e)}'}{Style.RESET_ALL}")
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('bypass.stack_trace') if translator else 'Stack trace'}: {traceback.format_exc()}{Style.RESET_ALL}")
return False
-def main(translator=None):
- """Main function"""
- return bypass_version(translator)
+def run(translator: Any = None) -> bool:
+ """Main function to run the version bypass.
+
+ Args:
+ translator: Optional translator for internationalization
+
+ Returns:
+ bool: True if successful, False otherwise
+ """
+ try:
+ print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
+ print(f"{Fore.CYAN}{EMOJI['RESET']} {translator.get('bypass_version.title') if translator else 'Bypass Version Check'}{Style.RESET_ALL}")
+ print(f"{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
+
+ success = bypass_version(translator)
+
+ print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
+ input(f"{EMOJI['INFO']} {translator.get('bypass_version.press_enter') if translator else 'Press Enter to continue...'}")
+
+ return success
+ except Exception as e:
+ logger.error(f"Error in run function: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} An unexpected error occurred: {str(e)}{Style.RESET_ALL}")
+ return False
+
+def main(translator: Any = None) -> bool:
+ """Entry point when called directly.
+
+ Args:
+ translator: Optional translator for internationalization
+
+ Returns:
+ bool: True if successful, False otherwise
+ """
+ return run(translator)
if __name__ == "__main__":
- main()
\ No newline at end of file
+ try:
+ from main import translator as main_translator
+ main(main_translator)
+ except ImportError:
+ logger.warning("Failed to import translator from main.py, running without translation")
+ main(None)
+ except Exception as e:
+ logger.error(f"Error running bypass_version.py: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} An unexpected error occurred: {str(e)}{Style.RESET_ALL}")
\ No newline at end of file
diff --git a/check_user_authorized.py b/check_user_authorized.py
index de93aa6..72bfb54 100644
--- a/check_user_authorized.py
+++ b/check_user_authorized.py
@@ -4,10 +4,20 @@ import time
import hashlib
import base64
import struct
+import logging
from colorama import Fore, Style, init
+from typing import Optional, Dict, Union, Any, Tuple
# Initialize colorama
-init()
+init(autoreset=True)
+
+# Configure logging
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S"
+)
+logger = logging.getLogger(__name__)
# Define emoji constants
EMOJI = {
@@ -20,22 +30,57 @@ EMOJI = {
}
def generate_hashed64_hex(input_str: str, salt: str = '') -> str:
- """Generate a SHA-256 hash of input + salt and return as hex"""
+ """Generate a SHA-256 hash of input + salt and return as hex.
+
+ Args:
+ input_str: The input string to hash
+ salt: Optional salt to add to the input string
+
+ Returns:
+ str: Hexadecimal representation of the hash
+ """
+ if not input_str:
+ logger.warning("Empty input string provided for hashing")
+ return ""
+
hash_obj = hashlib.sha256()
hash_obj.update((input_str + salt).encode('utf-8'))
return hash_obj.hexdigest()
def obfuscate_bytes(byte_array: bytearray) -> bytearray:
- """Obfuscate bytes using the algorithm from utils.js"""
+ """Obfuscate bytes using the algorithm from utils.js.
+
+ Args:
+ byte_array: The byte array to obfuscate
+
+ Returns:
+ bytearray: The obfuscated byte array
+ """
+ if not byte_array:
+ return bytearray()
+
t = 165
for r in range(len(byte_array)):
byte_array[r] = ((byte_array[r] ^ t) + (r % 256)) & 0xFF
t = byte_array[r]
return byte_array
-def generate_cursor_checksum(token: str, translator=None) -> str:
- """Generate Cursor checksum from token using the algorithm"""
+def generate_cursor_checksum(token: str, translator: Any = None) -> str:
+ """Generate Cursor checksum from token using the algorithm.
+
+ Args:
+ token: The authentication token
+ translator: Optional translator for internationalization
+
+ Returns:
+ str: The generated checksum
+ """
try:
+ # Validate input
+ if not token or not isinstance(token, str):
+ logger.error("Invalid token provided")
+ return ""
+
# Clean the token
clean_token = token.strip()
@@ -54,15 +99,16 @@ def generate_cursor_checksum(token: str, translator=None) -> str:
# Combine final checksum
return f"{encoded_checksum}{machine_id}/{mac_machine_id}"
except Exception as e:
+ logger.error(f"Error generating checksum: {e}")
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.error_generating_checksum', error=str(e)) if translator else f'Error generating checksum: {str(e)}'}{Style.RESET_ALL}")
return ""
-def check_user_authorized(token: str, translator=None) -> bool:
+def check_user_authorized(token: str, translator: Any = None) -> bool:
"""
- Check if the user is authorized with the given token
+ Check if the user is authorized with the given token.
Args:
- token (str): The authorization token
+ token: The authorization token
translator: Optional translator for internationalization
Returns:
@@ -71,6 +117,12 @@ def check_user_authorized(token: str, translator=None) -> bool:
try:
print(f"{Fore.CYAN}{EMOJI['CHECK']} {translator.get('auth_check.checking_authorization') if translator else 'Checking authorization...'}{Style.RESET_ALL}")
+ # Validate input
+ if not token or not isinstance(token, str):
+ logger.error("Invalid token provided")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.invalid_token') if translator else 'Invalid token'}{Style.RESET_ALL}")
+ return False
+
# Clean the token
if token and '%3A%3A' in token:
token = token.split('%3A%3A')[1]
@@ -81,6 +133,7 @@ def check_user_authorized(token: str, translator=None) -> bool:
token = token.strip()
if not token or len(token) < 10: # Add a basic validation for token length
+ logger.error("Token too short or empty after cleaning")
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.invalid_token') if translator else 'Invalid token'}{Style.RESET_ALL}")
return False
@@ -90,7 +143,10 @@ def check_user_authorized(token: str, translator=None) -> bool:
try:
# Generate checksum
checksum = generate_cursor_checksum(token, translator)
-
+ if not checksum:
+ logger.error("Failed to generate checksum")
+ return False
+
# Create request headers
headers = {
'accept-encoding': 'gzip',
@@ -107,53 +163,137 @@ def check_user_authorized(token: str, translator=None) -> bool:
print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('auth_check.checking_usage_information') if translator else 'Checking usage information...'}{Style.RESET_ALL}")
- # Make the request - this endpoint doesn't need a request body
- usage_response = requests.post(
- 'https://api2.cursor.sh/aiserver.v1.DashboardService/GetUsageBasedPremiumRequests',
- headers=headers,
- data=b'', # Empty body
- timeout=10
- )
+ # Make the request with timeout and retry
+ max_retries = 3
+ for attempt in range(max_retries):
+ try:
+ usage_response = requests.post(
+ 'https://api2.cursor.sh/aiserver.v1.DashboardService/GetUsageBasedPremiumRequests',
+ headers=headers,
+ data=b'', # Empty body
+ timeout=10
+ )
+ break
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
+ if attempt < max_retries - 1:
+ logger.warning(f"Request attempt {attempt + 1} failed: {e}. Retrying...")
+ time.sleep(2)
+ else:
+ raise
print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('auth_check.usage_response', response=usage_response.status_code) if translator else f'Usage response status: {usage_response.status_code}'}{Style.RESET_ALL}")
if usage_response.status_code == 200:
+ logger.info("User is authorized")
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('auth_check.user_authorized') if translator else 'User is authorized'}{Style.RESET_ALL}")
return True
elif usage_response.status_code == 401 or usage_response.status_code == 403:
+ logger.warning("User is unauthorized")
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.user_unauthorized') if translator else 'User is unauthorized'}{Style.RESET_ALL}")
return False
else:
+ logger.warning(f"Unexpected status code: {usage_response.status_code}")
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.unexpected_status_code', code=usage_response.status_code) if translator else f'Unexpected status code: {usage_response.status_code}'}{Style.RESET_ALL}")
# If the token at least looks like a valid JWT, consider it valid
if token.startswith('eyJ') and '.' in token and len(token) > 100:
+ logger.info("Token appears to be in JWT format, but API check returned an unexpected status code")
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.jwt_token_warning') if translator else 'Token appears to be in JWT format, but API check returned an unexpected status code. The token might be valid but API access is restricted.'}{Style.RESET_ALL}")
return True
return False
+ except requests.exceptions.Timeout:
+ logger.error("Request timed out")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.request_timeout') if translator else 'Request timed out'}{Style.RESET_ALL}")
+
+ # If the token at least looks like a valid JWT, consider it valid even if the API check fails
+ if token.startswith('eyJ') and '.' in token and len(token) > 100:
+ logger.info("Token appears to be in JWT format, but request timed out")
+ print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.jwt_token_warning') if translator else 'Token appears to be in JWT format, but API check timed out. The token might be valid but API access is restricted.'}{Style.RESET_ALL}")
+ return True
+
+ return False
+ except requests.exceptions.ConnectionError:
+ logger.error("Connection error")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.connection_error') if translator else 'Connection error'}{Style.RESET_ALL}")
+
+ # If the token at least looks like a valid JWT, consider it valid even if the API check fails
+ if token.startswith('eyJ') and '.' in token and len(token) > 100:
+ logger.info("Token appears to be in JWT format, but connection failed")
+ print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.jwt_token_warning') if translator else 'Token appears to be in JWT format, but API connection failed. The token might be valid but API access is restricted.'}{Style.RESET_ALL}")
+ return True
+
+ return False
except Exception as e:
+ logger.error(f"Error checking usage: {e}")
print(f"{Fore.YELLOW}{EMOJI['WARNING']} Error checking usage: {str(e)}{Style.RESET_ALL}")
# If the token at least looks like a valid JWT, consider it valid even if the API check fails
if token.startswith('eyJ') and '.' in token and len(token) > 100:
+ logger.info("Token appears to be in JWT format, but API check failed")
print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.jwt_token_warning') if translator else 'Token appears to be in JWT format, but API check failed. The token might be valid but API access is restricted.'}{Style.RESET_ALL}")
return True
return False
except requests.exceptions.Timeout:
+ logger.error("Request timed out")
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.request_timeout') if translator else 'Request timed out'}{Style.RESET_ALL}")
return False
except requests.exceptions.ConnectionError:
+ logger.error("Connection error")
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.connection_error') if translator else 'Connection error'}{Style.RESET_ALL}")
return False
except Exception as e:
+ logger.error(f"Error checking authorization: {e}")
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.check_error', error=str(e)) if translator else f'Error checking authorization: {str(e)}'}{Style.RESET_ALL}")
return False
-def run(translator=None):
- """Run function to be called from main.py"""
+def get_token_from_database(translator: Any = None) -> str:
+ """
+ Get token from database using cursor_acc_info.py.
+
+ Args:
+ translator: Optional translator for internationalization
+
+ Returns:
+ str: The token if found, empty string otherwise
+ """
+ try:
+ print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('auth_check.getting_token_from_db') if translator else 'Getting token from database...'}{Style.RESET_ALL}")
+
+ # Import functions from cursor_acc_info.py
+ from cursor_acc_info import get_token
+
+ # Get token using the get_token function
+ token = get_token()
+
+ if token:
+ logger.info("Token found in database")
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('auth_check.token_found_in_db') if translator else 'Token found in database'}{Style.RESET_ALL}")
+ return token
+ else:
+ logger.warning("Token not found in database")
+ print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.token_not_found_in_db') if translator else 'Token not found in database'}{Style.RESET_ALL}")
+ return ""
+ except ImportError:
+ logger.error("cursor_acc_info.py not found")
+ print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.cursor_acc_info_not_found') if translator else 'cursor_acc_info.py not found'}{Style.RESET_ALL}")
+ return ""
+ except Exception as e:
+ logger.error(f"Error getting token from database: {e}")
+ print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.error_getting_token_from_db', error=str(e)) if translator else f'Error getting token from database: {str(e)}'}{Style.RESET_ALL}")
+ return ""
+
+def run(translator: Any = None) -> bool:
+ """Run function to be called from main.py.
+
+ Args:
+ translator: Optional translator for internationalization
+
+ Returns:
+ bool: True if authorization successful, False otherwise
+ """
try:
# Ask user if they want to get token from database or input manually
choice = input(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('auth_check.token_source') if translator else 'Get token from database or input manually? (d/m, default: d): '}{Style.RESET_ALL}").strip().lower()
@@ -162,23 +302,7 @@ def run(translator=None):
# If user chooses database or default
if not choice or choice == 'd':
- print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('auth_check.getting_token_from_db') if translator else 'Getting token from database...'}{Style.RESET_ALL}")
-
- try:
- # Import functions from cursor_acc_info.py
- from cursor_acc_info import get_token
-
- # Get token using the get_token function
- token = get_token()
-
- if token:
- print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('auth_check.token_found_in_db') if translator else 'Token found in database'}{Style.RESET_ALL}")
- else:
- print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.token_not_found_in_db') if translator else 'Token not found in database'}{Style.RESET_ALL}")
- except ImportError:
- print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.cursor_acc_info_not_found') if translator else 'cursor_acc_info.py not found'}{Style.RESET_ALL}")
- except Exception as e:
- print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.error_getting_token_from_db', error=str(e)) if translator else f'Error getting token from database: {str(e)}'}{Style.RESET_ALL}")
+ token = get_token_from_database(translator)
# If token not found in database or user chooses manual input
if not token:
@@ -193,22 +317,50 @@ def run(translator=None):
is_authorized = check_user_authorized(token, translator)
if is_authorized:
+ logger.info("Authorization successful")
print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('auth_check.authorization_successful') if translator else 'Authorization successful!'}{Style.RESET_ALL}")
else:
+ logger.warning("Authorization failed")
print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.authorization_failed') if translator else 'Authorization failed!'}{Style.RESET_ALL}")
return is_authorized
-
- except KeyboardInterrupt:
- print(f"\n{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('auth_check.operation_cancelled') if translator else 'Operation cancelled by user'}{Style.RESET_ALL}")
- return False
except Exception as e:
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('auth_check.unexpected_error', error=str(e)) if translator else f'Unexpected error: {str(e)}'}{Style.RESET_ALL}")
+ logger.error(f"Error in run function: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} An unexpected error occurred: {str(e)}{Style.RESET_ALL}")
return False
-def main(translator=None):
- """Main function to check user authorization"""
- return run(translator)
+def main(translator: Any = None) -> bool:
+ """Main function to be called when script is run directly.
+
+ Args:
+ translator: Optional translator for internationalization
+
+ Returns:
+ bool: True if authorization successful, False otherwise
+ """
+ try:
+ print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
+ print(f"{Fore.CYAN}{EMOJI['CHECK']} {translator.get('auth_check.title') if translator else 'Check User Authorization'}{Style.RESET_ALL}")
+ print(f"{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
+
+ result = run(translator)
+
+ print(f"\n{Fore.CYAN}{'='*50}{Style.RESET_ALL}")
+ input(f"{EMOJI['INFO']} {translator.get('auth_check.press_enter') if translator else 'Press Enter to continue...'}")
+
+ return result
+ except Exception as e:
+ logger.error(f"Error in main function: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} An unexpected error occurred: {str(e)}{Style.RESET_ALL}")
+ return False
if __name__ == "__main__":
- main()
\ No newline at end of file
+ try:
+ from main import translator as main_translator
+ main(main_translator)
+ except ImportError:
+ logger.warning("Failed to import translator from main.py, running without translation")
+ main(None)
+ except Exception as e:
+ logger.error(f"Error running check_user_authorized.py: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} An unexpected error occurred: {str(e)}{Style.RESET_ALL}")
\ No newline at end of file
diff --git a/config.py b/config.py
index f59f384..f1f6a69 100644
--- a/config.py
+++ b/config.py
@@ -1,11 +1,29 @@
import os
import sys
import configparser
-from colorama import Fore, Style
-from utils import get_user_documents_path, get_linux_cursor_path, get_default_driver_path, get_default_browser_path
-import shutil
+import logging
+import tempfile
import datetime
+import platform
+from pathlib import Path
+from typing import Optional, Dict, Any, Union, List, Tuple
+from colorama import Fore, Style, init
+# Initialize colorama
+init(autoreset=True)
+
+# Configure logging
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S"
+)
+logger = logging.getLogger(__name__)
+
+# Import utils after logging setup to avoid circular imports
+from utils import get_user_documents_path, get_linux_cursor_path, get_default_driver_path, get_default_browser_path
+
+# Define emoji constants
EMOJI = {
"INFO": "โน๏ธ",
"WARNING": "โ ๏ธ",
@@ -15,45 +33,103 @@ EMOJI = {
"ARROW": "โก๏ธ",
"USER": "๐ค",
"KEY": "๐",
- "SETTINGS": "โ๏ธ"
+ "SETTINGS": "โ๏ธ",
+ "CONFIG": "๐"
}
-# global config cache
+# Global config cache
_config_cache = None
-def setup_config(translator=None):
- """Setup configuration file and return config object"""
- try:
- # get documents path
+class ConfigManager:
+ """Class to manage configuration operations"""
+
+ def __init__(self, translator: Any = None):
+ """Initialize ConfigManager
+
+ Args:
+ translator: Optional translator for internationalization
+ """
+ self.translator = translator
+ self.config = configparser.ConfigParser()
+ self.config_dir = None
+ self.config_file = None
+
+ def _get_message(self, key: str, fallback: str, **kwargs) -> str:
+ """Get translated message or fallback
+
+ Args:
+ key: Translation key
+ fallback: Fallback message if translation not available
+ **kwargs: Format parameters for the message
+
+ Returns:
+ str: Translated or fallback message
+ """
+ if self.translator:
+ return self.translator.get(key, fallback=fallback, **kwargs)
+ return fallback.format(**kwargs) if kwargs else fallback
+
+ def setup_config_directory(self) -> Tuple[str, str]:
+ """Setup configuration directory
+
+ Returns:
+ Tuple[str, str]: Configuration directory and file paths
+ """
+ # Get documents path
docs_path = get_user_documents_path()
if not docs_path or not os.path.exists(docs_path):
- # if documents path not found, use current directory
- print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('config.documents_path_not_found', fallback='Documents path not found, using current directory') if translator else 'Documents path not found, using current directory'}{Style.RESET_ALL}")
+ # If documents path not found, use current directory
+ msg = self._get_message('config.documents_path_not_found',
+ 'Documents path not found, using current directory')
+ logger.warning(msg)
+ print(f"{Fore.YELLOW}{EMOJI['WARNING']} {msg}{Style.RESET_ALL}")
docs_path = os.path.abspath('.')
- # normalize path
+ # Normalize path
config_dir = os.path.normpath(os.path.join(docs_path, ".cursor-free-vip"))
config_file = os.path.normpath(os.path.join(config_dir, "config.ini"))
- # create config directory, only print message when directory not exists
+ # Create config directory
dir_exists = os.path.exists(config_dir)
try:
os.makedirs(config_dir, exist_ok=True)
- if not dir_exists: # only print message when directory not exists
- print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('config.config_dir_created', path=config_dir) if translator else f'Config directory created: {config_dir}'}{Style.RESET_ALL}")
+ if not dir_exists:
+ msg = self._get_message('config.config_dir_created',
+ 'Config directory created: {path}', path=config_dir)
+ logger.info(f"Config directory created: {config_dir}")
+ print(f"{Fore.CYAN}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
except Exception as e:
- # if cannot create directory, use temporary directory
- import tempfile
+ # If cannot create directory, use temporary directory
+ logger.warning(f"Failed to create config directory: {e}")
temp_dir = os.path.normpath(os.path.join(tempfile.gettempdir(), ".cursor-free-vip"))
temp_exists = os.path.exists(temp_dir)
config_dir = temp_dir
config_file = os.path.normpath(os.path.join(config_dir, "config.ini"))
- os.makedirs(config_dir, exist_ok=True)
- if not temp_exists: # only print message when temporary directory not exists
- print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('config.using_temp_dir', path=config_dir, error=str(e)) if translator else f'Using temporary directory due to error: {config_dir} (Error: {str(e)})'}{Style.RESET_ALL}")
+
+ try:
+ os.makedirs(config_dir, exist_ok=True)
+ if not temp_exists:
+ msg = self._get_message('config.using_temp_dir',
+ 'Using temporary directory due to error: {path} (Error: {error})',
+ path=config_dir, error=str(e))
+ print(f"{Fore.YELLOW}{EMOJI['WARNING']} {msg}{Style.RESET_ALL}")
+ except Exception as inner_e:
+ logger.error(f"Failed to create temporary config directory: {inner_e}")
+ # Last resort: use current directory
+ config_dir = os.path.abspath('.')
+ config_file = os.path.join(config_dir, "config.ini")
+
+ self.config_dir = config_dir
+ self.config_file = config_file
+ return config_dir, config_file
+
+ def get_default_config(self) -> Dict[str, Dict[str, Any]]:
+ """Get default configuration
- # create config object
- config = configparser.ConfigParser()
+ Returns:
+ Dict[str, Dict[str, Any]]: Default configuration dictionary
+ """
+ config_dir = self.config_dir or os.path.join(get_user_documents_path(), ".cursor-free-vip")
# Default configuration
default_config = {
@@ -70,7 +146,7 @@ def setup_config(translator=None):
'opera_path': get_default_browser_path('opera'),
'opera_driver_path': get_default_driver_path('opera'),
'operagx_path': get_default_browser_path('operagx'),
- 'operagx_driver_path': get_default_driver_path('chrome') # Opera GX ไฝฟ็จ Chrome ้ฉฑๅจ
+ 'operagx_driver_path': get_default_driver_path('chrome') # Opera GX uses Chrome driver
},
'Turnstile': {
'handle_turnstile_time': '2',
@@ -98,13 +174,13 @@ def setup_config(translator=None):
'enabled_account_info': 'True'
},
'OAuth': {
- 'show_selection_alert': False, # ้ป่ฎคไธๆพ็คบ้ๆฉๆ็คบๅผน็ช
- 'timeout': 120,
- 'max_attempts': 3
+ 'show_selection_alert': 'False',
+ 'timeout': '120',
+ 'max_attempts': '3'
},
'Token': {
'refresh_server': 'https://token.cursorpro.com.cn',
- 'enable_refresh': True
+ 'enable_refresh': 'True'
},
'Language': {
'current_language': '', # Set by local system detection if empty
@@ -115,266 +191,411 @@ def setup_config(translator=None):
}
# Add system-specific path configuration
+ self._add_system_paths(default_config)
+
+ return default_config
+
+ def _add_system_paths(self, default_config: Dict[str, Dict[str, Any]]) -> None:
+ """Add system-specific paths to the default configuration
+
+ Args:
+ default_config: Default configuration dictionary to update
+ """
if sys.platform == "win32":
- appdata = os.getenv("APPDATA")
- localappdata = os.getenv("LOCALAPPDATA", "")
- default_config['WindowsPaths'] = {
- 'storage_path': os.path.join(appdata, "Cursor", "User", "globalStorage", "storage.json"),
- 'sqlite_path': os.path.join(appdata, "Cursor", "User", "globalStorage", "state.vscdb"),
- 'machine_id_path': os.path.join(appdata, "Cursor", "machineId"),
- 'cursor_path': os.path.join(localappdata, "Programs", "Cursor", "resources", "app"),
- 'updater_path': os.path.join(localappdata, "cursor-updater"),
- 'update_yml_path': os.path.join(localappdata, "Programs", "Cursor", "resources", "app-update.yml"),
- 'product_json_path': os.path.join(localappdata, "Programs", "Cursor", "resources", "app", "product.json")
- }
- # Create storage directory
- os.makedirs(os.path.dirname(default_config['WindowsPaths']['storage_path']), exist_ok=True)
-
+ self._add_windows_paths(default_config)
elif sys.platform == "darwin":
- default_config['MacPaths'] = {
- 'storage_path': os.path.abspath(os.path.expanduser("~/Library/Application Support/Cursor/User/globalStorage/storage.json")),
- 'sqlite_path': os.path.abspath(os.path.expanduser("~/Library/Application Support/Cursor/User/globalStorage/state.vscdb")),
- 'machine_id_path': os.path.expanduser("~/Library/Application Support/Cursor/machineId"),
- 'cursor_path': "/Applications/Cursor.app/Contents/Resources/app",
- 'updater_path': os.path.expanduser("~/Library/Application Support/cursor-updater"),
- 'update_yml_path': "/Applications/Cursor.app/Contents/Resources/app-update.yml",
- 'product_json_path': "/Applications/Cursor.app/Contents/Resources/app/product.json"
- }
- # Create storage directory
- os.makedirs(os.path.dirname(default_config['MacPaths']['storage_path']), exist_ok=True)
-
+ self._add_macos_paths(default_config)
elif sys.platform == "linux":
- # Get the actual user's home directory, handling both sudo and normal cases
- sudo_user = os.environ.get('SUDO_USER')
- current_user = sudo_user if sudo_user else (os.getenv('USER') or os.getenv('USERNAME'))
-
- if not current_user:
- current_user = os.path.expanduser('~').split('/')[-1]
-
- # Handle sudo case
- if sudo_user:
- actual_home = f"/home/{sudo_user}"
- root_home = "/root"
- else:
- actual_home = f"/home/{current_user}"
- root_home = None
-
- if not os.path.exists(actual_home):
- actual_home = os.path.expanduser("~")
-
- # Define base config directory
- config_base = os.path.join(actual_home, ".config")
-
- # Try both "Cursor" and "cursor" directory names in both user and root locations
- cursor_dir = None
- possible_paths = [
- os.path.join(config_base, "Cursor"),
- os.path.join(config_base, "cursor"),
- os.path.join(root_home, ".config", "Cursor") if root_home else None,
- os.path.join(root_home, ".config", "cursor") if root_home else None
- ]
-
- for path in possible_paths:
- if path and os.path.exists(path):
- cursor_dir = path
- break
-
- if not cursor_dir:
- print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('config.neither_cursor_nor_cursor_directory_found', config_base=config_base) if translator else f'Neither Cursor nor cursor directory found in {config_base}'}{Style.RESET_ALL}")
- if root_home:
- print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.also_checked', path=f'{root_home}/.config') if translator else f'Also checked {root_home}/.config'}{Style.RESET_ALL}")
- print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.please_make_sure_cursor_is_installed_and_has_been_run_at_least_once') if translator else 'Please make sure Cursor is installed and has been run at least once'}{Style.RESET_ALL}")
-
- # Define Linux paths using the found cursor directory
- storage_path = os.path.abspath(os.path.join(cursor_dir, "User/globalStorage/storage.json")) if cursor_dir else ""
- storage_dir = os.path.dirname(storage_path) if storage_path else ""
-
- # Verify paths and permissions
- try:
- # Check storage directory
- if storage_dir and not os.path.exists(storage_dir):
- print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('config.storage_directory_not_found', storage_dir=storage_dir) if translator else f'Storage directory not found: {storage_dir}'}{Style.RESET_ALL}")
- print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.please_make_sure_cursor_is_installed_and_has_been_run_at_least_once') if translator else 'Please make sure Cursor is installed and has been run at least once'}{Style.RESET_ALL}")
-
- # Check storage.json with more detailed verification
- if storage_path and os.path.exists(storage_path):
- # Get file stats
- try:
- stat = os.stat(storage_path)
- print(f"{Fore.GREEN}{EMOJI['INFO']} {translator.get('config.storage_file_found', storage_path=storage_path) if translator else f'Storage file found: {storage_path}'}{Style.RESET_ALL}")
- print(f"{Fore.GREEN}{EMOJI['INFO']} {translator.get('config.file_size', size=stat.st_size) if translator else f'File size: {stat.st_size} bytes'}{Style.RESET_ALL}")
- print(f"{Fore.GREEN}{EMOJI['INFO']} {translator.get('config.file_permissions', permissions=oct(stat.st_mode & 0o777)) if translator else f'File permissions: {oct(stat.st_mode & 0o777)}'}{Style.RESET_ALL}")
- print(f"{Fore.GREEN}{EMOJI['INFO']} {translator.get('config.file_owner', owner=stat.st_uid) if translator else f'File owner: {stat.st_uid}'}{Style.RESET_ALL}")
- print(f"{Fore.GREEN}{EMOJI['INFO']} {translator.get('config.file_group', group=stat.st_gid) if translator else f'File group: {stat.st_gid}'}{Style.RESET_ALL}")
- except Exception as e:
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('config.error_getting_file_stats', error=str(e)) if translator else f'Error getting file stats: {str(e)}'}{Style.RESET_ALL}")
-
- # Check if file is readable and writable
- if not os.access(storage_path, os.R_OK | os.W_OK):
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('config.permission_denied', storage_path=storage_path) if translator else f'Permission denied: {storage_path}'}{Style.RESET_ALL}")
- if sudo_user:
- print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.try_running', command=f'chown {sudo_user}:{sudo_user} {storage_path}') if translator else f'Try running: chown {sudo_user}:{sudo_user} {storage_path}'}{Style.RESET_ALL}")
- print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.and') if translator else 'And'}: chmod 644 {storage_path}{Style.RESET_ALL}")
- else:
- print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.try_running', command=f'chown {current_user}:{current_user} {storage_path}') if translator else f'Try running: chown {current_user}:{current_user} {storage_path}'}{Style.RESET_ALL}")
- print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.and') if translator else 'And'}: chmod 644 {storage_path}{Style.RESET_ALL}")
-
- # Try to read the file to verify it's not corrupted
- try:
- with open(storage_path, 'r') as f:
- content = f.read()
- if not content.strip():
- print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('config.storage_file_is_empty', storage_path=storage_path) if translator else f'Storage file is empty: {storage_path}'}{Style.RESET_ALL}")
- print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.the_file_might_be_corrupted_please_reinstall_cursor') if translator else 'The file might be corrupted, please reinstall Cursor'}{Style.RESET_ALL}")
- else:
- print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('config.storage_file_is_valid_and_contains_data') if translator else 'Storage file is valid and contains data'}{Style.RESET_ALL}")
- except Exception as e:
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('config.error_reading_storage_file', error=str(e)) if translator else f'Error reading storage file: {str(e)}'}{Style.RESET_ALL}")
- print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.the_file_might_be_corrupted_please_reinstall_cursor') if translator else 'The file might be corrupted. Please reinstall Cursor'}{Style.RESET_ALL}")
- elif storage_path:
- print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('config.storage_file_not_found', storage_path=storage_path) if translator else f'Storage file not found: {storage_path}'}{Style.RESET_ALL}")
- print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.please_make_sure_cursor_is_installed_and_has_been_run_at_least_once') if translator else 'Please make sure Cursor is installed and has been run at least once'}{Style.RESET_ALL}")
-
- except (OSError, IOError) as e:
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('config.error_checking_linux_paths', error=str(e)) if translator else f'Error checking Linux paths: {str(e)}'}{Style.RESET_ALL}")
-
- # Define all paths using the found cursor directory
- default_config['LinuxPaths'] = {
- 'storage_path': storage_path,
- 'sqlite_path': os.path.abspath(os.path.join(cursor_dir, "User/globalStorage/state.vscdb")) if cursor_dir else "",
- 'machine_id_path': os.path.join(cursor_dir, "machineid") if cursor_dir else "",
- 'cursor_path': get_linux_cursor_path(),
- 'updater_path': os.path.join(config_base, "cursor-updater"),
- 'update_yml_path': os.path.join(cursor_dir, "resources/app-update.yml") if cursor_dir else "",
- 'product_json_path': os.path.join(cursor_dir, "resources/app/product.json") if cursor_dir else ""
- }
-
- # Add tempmail_plus configuration
- default_config['TempMailPlus'] = {
- 'enabled': 'false',
- 'email': '',
- 'epin': ''
- }
-
- # Read existing configuration and merge
- if os.path.exists(config_file):
- config.read(config_file, encoding='utf-8')
- config_modified = False
-
- for section, options in default_config.items():
- if not config.has_section(section):
- config.add_section(section)
- config_modified = True
- for option, value in options.items():
- if not config.has_option(section, option):
- config.set(section, option, str(value))
- config_modified = True
- if translator:
- print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('config.config_option_added', option=f'{section}.{option}') if translator else f'Config option added: {section}.{option}'}{Style.RESET_ALL}")
-
- if config_modified:
- with open(config_file, 'w', encoding='utf-8') as f:
- config.write(f)
- if translator:
- print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('config.config_updated') if translator else 'Config updated'}{Style.RESET_ALL}")
- else:
- for section, options in default_config.items():
- config.add_section(section)
- for option, value in options.items():
- config.set(section, option, str(value))
-
- with open(config_file, 'w', encoding='utf-8') as f:
- config.write(f)
- if translator:
- print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('config.config_created', config_file=config_file) if translator else f'Config created: {config_file}'}{Style.RESET_ALL}")
-
- return config
-
- except Exception as e:
- if translator:
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('config.config_setup_error', error=str(e)) if translator else f'Error setting up config: {str(e)}'}{Style.RESET_ALL}")
- return None
+ self._add_linux_paths(default_config)
-def print_config(config, translator=None):
- """Print configuration in a readable format"""
+ def _add_windows_paths(self, default_config: Dict[str, Dict[str, Any]]) -> None:
+ """Add Windows-specific paths to the default configuration
+
+ Args:
+ default_config: Default configuration dictionary to update
+ """
+ appdata = os.getenv("APPDATA", "")
+ localappdata = os.getenv("LOCALAPPDATA", "")
+
+ if not appdata or not localappdata:
+ logger.warning("APPDATA or LOCALAPPDATA environment variables not found")
+ appdata = os.path.expanduser("~\\AppData\\Roaming")
+ localappdata = os.path.expanduser("~\\AppData\\Local")
+
+ default_config['WindowsPaths'] = {
+ 'storage_path': os.path.join(appdata, "Cursor", "User", "globalStorage", "storage.json"),
+ 'sqlite_path': os.path.join(appdata, "Cursor", "User", "globalStorage", "state.vscdb"),
+ 'machine_id_path': os.path.join(appdata, "Cursor", "machineId"),
+ 'cursor_path': os.path.join(localappdata, "Programs", "Cursor", "resources", "app"),
+ 'updater_path': os.path.join(localappdata, "cursor-updater"),
+ 'update_yml_path': os.path.join(localappdata, "Programs", "Cursor", "resources", "app-update.yml"),
+ 'product_json_path': os.path.join(localappdata, "Programs", "Cursor", "resources", "app", "product.json")
+ }
+
+ # Create storage directory
+ try:
+ storage_dir = os.path.dirname(default_config['WindowsPaths']['storage_path'])
+ os.makedirs(storage_dir, exist_ok=True)
+ except Exception as e:
+ logger.warning(f"Failed to create storage directory: {e}")
+
+ def _add_macos_paths(self, default_config: Dict[str, Dict[str, Any]]) -> None:
+ """Add macOS-specific paths to the default configuration
+
+ Args:
+ default_config: Default configuration dictionary to update
+ """
+ default_config['MacPaths'] = {
+ 'storage_path': os.path.abspath(os.path.expanduser("~/Library/Application Support/Cursor/User/globalStorage/storage.json")),
+ 'sqlite_path': os.path.abspath(os.path.expanduser("~/Library/Application Support/Cursor/User/globalStorage/state.vscdb")),
+ 'machine_id_path': os.path.expanduser("~/Library/Application Support/Cursor/machineId"),
+ 'cursor_path': "/Applications/Cursor.app/Contents/Resources/app",
+ 'updater_path': os.path.expanduser("~/Library/Application Support/cursor-updater"),
+ 'update_yml_path': "/Applications/Cursor.app/Contents/Resources/app-update.yml",
+ 'product_json_path': "/Applications/Cursor.app/Contents/Resources/app/product.json"
+ }
+
+ # Create storage directory
+ try:
+ storage_dir = os.path.dirname(default_config['MacPaths']['storage_path'])
+ os.makedirs(storage_dir, exist_ok=True)
+ except Exception as e:
+ logger.warning(f"Failed to create storage directory: {e}")
+
+ def _add_linux_paths(self, default_config: Dict[str, Dict[str, Any]]) -> None:
+ """Add Linux-specific paths to the default configuration
+
+ Args:
+ default_config: Default configuration dictionary to update
+ """
+ # Get the actual user's home directory, handling both sudo and normal cases
+ sudo_user = os.environ.get('SUDO_USER')
+ current_user = sudo_user if sudo_user else (os.getenv('USER') or os.getenv('USERNAME'))
+
+ if not current_user:
+ current_user = os.path.expanduser('~').split('/')[-1]
+
+ # Handle sudo case
+ if sudo_user:
+ actual_home = f"/home/{sudo_user}"
+ root_home = "/root"
+ else:
+ actual_home = f"/home/{current_user}"
+ root_home = None
+
+ if not os.path.exists(actual_home):
+ actual_home = os.path.expanduser("~")
+
+ # Define base config directory
+ config_base = os.path.join(actual_home, ".config")
+
+ # Try both "Cursor" and "cursor" directory names in both user and root locations
+ cursor_dir = None
+ possible_paths = [
+ os.path.join(config_base, "Cursor"),
+ os.path.join(config_base, "cursor"),
+ os.path.join(root_home, ".config", "Cursor") if root_home else None,
+ os.path.join(root_home, ".config", "cursor") if root_home else None
+ ]
+
+ for path in possible_paths:
+ if path and os.path.exists(path):
+ cursor_dir = path
+ break
+
+ if not cursor_dir:
+ msg = self._get_message('config.neither_cursor_nor_cursor_directory_found',
+ 'Neither Cursor nor cursor directory found in {config_base}',
+ config_base=config_base)
+ logger.warning(f"Cursor directory not found in {config_base}")
+ print(f"{Fore.YELLOW}{EMOJI['WARNING']} {msg}{Style.RESET_ALL}")
+
+ if root_home:
+ msg = self._get_message('config.also_checked',
+ 'Also checked {path}',
+ path=f'{root_home}/.config')
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
+
+ msg = self._get_message('config.please_make_sure_cursor_is_installed_and_has_been_run_at_least_once',
+ 'Please make sure Cursor is installed and has been run at least once')
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
+
+ # Define Linux paths using the found cursor directory
+ storage_path = os.path.abspath(os.path.join(cursor_dir, "User/globalStorage/storage.json")) if cursor_dir else ""
+ storage_dir = os.path.dirname(storage_path) if storage_path else ""
+
+ # Set default Linux paths
+ default_config['LinuxPaths'] = {
+ 'storage_path': storage_path,
+ 'sqlite_path': os.path.abspath(os.path.join(cursor_dir, "User/globalStorage/state.vscdb")) if cursor_dir else "",
+ 'machine_id_path': os.path.join(cursor_dir, "machineid") if cursor_dir else "",
+ 'cursor_path': get_linux_cursor_path(),
+ 'updater_path': os.path.join(config_base, "cursor-updater"),
+ 'update_yml_path': os.path.join(cursor_dir, "resources/app-update.yml") if cursor_dir else "",
+ 'product_json_path': os.path.join(cursor_dir, "resources/app/product.json") if cursor_dir else ""
+ }
+
+ # Verify paths and permissions
+ self._verify_linux_paths(storage_path, storage_dir)
+
+ def _verify_linux_paths(self, storage_path: str, storage_dir: str) -> None:
+ """Verify Linux paths and permissions
+
+ Args:
+ storage_path: Path to the storage.json file
+ storage_dir: Directory containing the storage.json file
+ """
+ try:
+ # Check storage directory
+ if storage_dir and not os.path.exists(storage_dir):
+ msg = self._get_message('config.storage_directory_not_found',
+ 'Storage directory not found: {storage_dir}',
+ storage_dir=storage_dir)
+ logger.warning(f"Storage directory not found: {storage_dir}")
+ print(f"{Fore.YELLOW}{EMOJI['WARNING']} {msg}{Style.RESET_ALL}")
+
+ msg = self._get_message('config.please_make_sure_cursor_is_installed_and_has_been_run_at_least_once',
+ 'Please make sure Cursor is installed and has been run at least once')
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
+
+ # Check storage.json with more detailed verification
+ if storage_path and os.path.exists(storage_path):
+ # Get file stats
+ try:
+ stat = os.stat(storage_path)
+ msg = self._get_message('config.storage_file_found',
+ 'Storage file found: {storage_path}',
+ storage_path=storage_path)
+ print(f"{Fore.GREEN}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
+
+ # Log file details
+ file_details = [
+ ('config.file_size', 'File size: {size} bytes', {'size': stat.st_size}),
+ ('config.file_permissions', 'File permissions: {permissions}', {'permissions': oct(stat.st_mode & 0o777)}),
+ ('config.file_owner', 'File owner: {owner}', {'owner': stat.st_uid}),
+ ('config.file_group', 'File group: {group}', {'group': stat.st_gid})
+ ]
+
+ for key, fallback, kwargs in file_details:
+ msg = self._get_message(key, fallback, **kwargs)
+ print(f"{Fore.GREEN}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
+
+ except Exception as e:
+ logger.error(f"Error getting file stats: {e}")
+ msg = self._get_message('config.error_getting_file_stats',
+ 'Error getting file stats: {error}',
+ error=str(e))
+ print(f"{Fore.RED}{EMOJI['ERROR']} {msg}{Style.RESET_ALL}")
+
+ # Check if file is readable and writable
+ if not os.access(storage_path, os.R_OK | os.W_OK):
+ msg = self._get_message('config.permission_denied',
+ 'Permission denied: {storage_path}',
+ storage_path=storage_path)
+ logger.warning(f"Permission denied: {storage_path}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {msg}{Style.RESET_ALL}")
+
+ sudo_user = os.environ.get('SUDO_USER')
+ current_user = sudo_user if sudo_user else (os.getenv('USER') or os.getenv('USERNAME'))
+
+ if sudo_user:
+ cmd = f"chown {sudo_user}:{sudo_user} {storage_path}"
+ else:
+ cmd = f"chown {current_user}:{current_user} {storage_path}"
+
+ msg = self._get_message('config.try_running',
+ 'Try running: {command}',
+ command=cmd)
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
+
+ msg = self._get_message('config.and', 'And')
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} {msg}: chmod 644 {storage_path}{Style.RESET_ALL}")
+
+ # Try to read the file to verify it's not corrupted
+ try:
+ with open(storage_path, 'r') as f:
+ content = f.read()
+ if not content.strip():
+ msg = self._get_message('config.storage_file_is_empty',
+ 'Storage file is empty: {storage_path}',
+ storage_path=storage_path)
+ logger.warning(f"Storage file is empty: {storage_path}")
+ print(f"{Fore.YELLOW}{EMOJI['WARNING']} {msg}{Style.RESET_ALL}")
+
+ msg = self._get_message('config.the_file_might_be_corrupted_please_reinstall_cursor',
+ 'The file might be corrupted, please reinstall Cursor')
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
+ else:
+ msg = self._get_message('config.storage_file_is_valid_and_contains_data',
+ 'Storage file is valid and contains data')
+ logger.info("Storage file is valid and contains data")
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {msg}{Style.RESET_ALL}")
+ except Exception as e:
+ logger.error(f"Error reading storage file: {e}")
+ msg = self._get_message('config.error_reading_storage_file',
+ 'Error reading storage file: {error}',
+ error=str(e))
+ print(f"{Fore.RED}{EMOJI['ERROR']} {msg}{Style.RESET_ALL}")
+
+ msg = self._get_message('config.the_file_might_be_corrupted_please_reinstall_cursor',
+ 'The file might be corrupted. Please reinstall Cursor')
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
+ elif storage_path:
+ msg = self._get_message('config.storage_file_not_found',
+ 'Storage file not found: {storage_path}',
+ storage_path=storage_path)
+ logger.warning(f"Storage file not found: {storage_path}")
+ print(f"{Fore.YELLOW}{EMOJI['WARNING']} {msg}{Style.RESET_ALL}")
+
+ msg = self._get_message('config.please_make_sure_cursor_is_installed_and_has_been_run_at_least_once',
+ 'Please make sure Cursor is installed and has been run at least once')
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
+
+ except (OSError, IOError) as e:
+ logger.error(f"Error checking Linux paths: {e}")
+ msg = self._get_message('config.error_checking_linux_paths',
+ 'Error checking Linux paths: {error}',
+ error=str(e))
+ print(f"{Fore.RED}{EMOJI['ERROR']} {msg}{Style.RESET_ALL}")
+
+ def setup(self) -> configparser.ConfigParser:
+ """Setup configuration
+
+ Returns:
+ configparser.ConfigParser: Configured ConfigParser object
+ """
+ # Setup config directory
+ self.setup_config_directory()
+
+ # Get default configuration
+ default_config = self.get_default_config()
+
+ # Read existing config if it exists
+ if os.path.exists(self.config_file):
+ try:
+ self.config.read(self.config_file)
+ logger.info(f"Read existing configuration from {self.config_file}")
+ except Exception as e:
+ logger.error(f"Error reading config file: {e}")
+ # Continue with default config
+
+ # Update config with default values for missing sections/options
+ for section, options in default_config.items():
+ if not self.config.has_section(section):
+ self.config.add_section(section)
+
+ for option, value in options.items():
+ if not self.config.has_option(section, option):
+ self.config.set(section, option, str(value))
+
+ # Save config
+ try:
+ with open(self.config_file, 'w', encoding='utf-8') as f:
+ self.config.write(f)
+ logger.info(f"Configuration saved to {self.config_file}")
+ except Exception as e:
+ logger.error(f"Error saving config file: {e}")
+
+ return self.config
+
+def setup_config(translator: Any = None) -> configparser.ConfigParser:
+ """Setup configuration file and return config object
+
+ Args:
+ translator: Optional translator for internationalization
+
+ Returns:
+ configparser.ConfigParser: Configured ConfigParser object
+ """
+ try:
+ config_manager = ConfigManager(translator)
+ return config_manager.setup()
+ except Exception as e:
+ logger.error(f"Error setting up configuration: {e}")
+ # Return empty config as fallback
+ return configparser.ConfigParser()
+
+def print_config(config: configparser.ConfigParser, translator: Any = None) -> None:
+ """Print configuration
+
+ Args:
+ config: ConfigParser object
+ translator: Optional translator for internationalization
+ """
if not config:
- print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('config.config_not_available') if translator else 'Configuration not available'}{Style.RESET_ALL}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} Configuration not available{Style.RESET_ALL}")
return
- print(f"\n{Fore.CYAN}{EMOJI['INFO']} {translator.get('config.configuration') if translator else 'Configuration'}:{Style.RESET_ALL}")
- print(f"\n{Fore.CYAN}{'โ' * 70}{Style.RESET_ALL}")
- for section in config.sections():
- print(f"{Fore.GREEN}[{section}]{Style.RESET_ALL}")
- for key, value in config.items(section):
- # ๅฏนๅธๅฐๅผ่ฟ่ก็นๆฎๅค็๏ผไฝฟๅ
ถๆพ็คบไธบๅฝฉ่ฒ
- if value.lower() in ('true', 'yes', 'on', '1'):
- value_display = f"{Fore.GREEN}{translator.get('config.enabled') if translator else 'Enabled'}{Style.RESET_ALL}"
- elif value.lower() in ('false', 'no', 'off', '0'):
- value_display = f"{Fore.RED}{translator.get('config.disabled') if translator else 'Disabled'}{Style.RESET_ALL}"
- else:
- value_display = value
-
- print(f" {key} = {value_display}")
+ print(f"\n{Fore.CYAN}{EMOJI['CONFIG']} Configuration:{Style.RESET_ALL}")
- print(f"\n{Fore.CYAN}{'โ' * 70}{Style.RESET_ALL}")
- config_dir = os.path.join(get_user_documents_path(), ".cursor-free-vip", "config.ini")
- print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('config.config_directory') if translator else 'Config Directory'}: {config_dir}{Style.RESET_ALL}")
+ for section in config.sections():
+ print(f"\n{Fore.CYAN}[{section}]{Style.RESET_ALL}")
+ for option in config.options(section):
+ value = config.get(section, option)
+ # Mask sensitive information
+ if 'token' in option.lower() or 'password' in option.lower() or 'key' in option.lower():
+ value = '*' * 8
+ print(f" {option} = {value}")
- print()
-
-def force_update_config(translator=None):
- """
- Force update configuration file with latest defaults if update check is enabled.
+def force_update_config(translator: Any = None) -> configparser.ConfigParser:
+ """Force update configuration
+
Args:
- translator: Translator instance
+ translator: Optional translator for internationalization
+
Returns:
- ConfigParser instance or None if failed
+ configparser.ConfigParser: Updated ConfigParser object
"""
+ global _config_cache
+ _config_cache = None
+
+ # Create backup of existing config
try:
config_dir = os.path.join(get_user_documents_path(), ".cursor-free-vip")
config_file = os.path.join(config_dir, "config.ini")
- current_time = datetime.datetime.now()
-
- # If the config file exists, check if forced update is enabled
+
if os.path.exists(config_file):
- # First, read the existing configuration
- existing_config = configparser.ConfigParser()
- existing_config.read(config_file, encoding='utf-8')
- # Check if "enabled_update_check" is True
- update_enabled = True # Default to True if not set
- if existing_config.has_section('Utils') and existing_config.has_option('Utils', 'enabled_force_update'):
- update_enabled = existing_config.get('Utils', 'enabled_force_update').strip().lower() in ('true', 'yes', '1', 'on')
-
- if update_enabled:
- try:
- # Create a backup
- backup_file = f"{config_file}.bak.{current_time.strftime('%Y%m%d_%H%M%S')}"
- shutil.copy2(config_file, backup_file)
- if translator:
- print(f"\n{Fore.CYAN}{EMOJI['INFO']} {translator.get('config.backup_created', path=backup_file) if translator else f'Backup created: {backup_file}'}{Style.RESET_ALL}")
- print(f"\n{Fore.CYAN}{EMOJI['INFO']} {translator.get('config.config_force_update_enabled') if translator else 'Config file force update enabled'}{Style.RESET_ALL}")
- # Delete the original config file (forced update)
- os.remove(config_file)
- if translator:
- print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('config.config_removed') if translator else 'Config file removed for forced update'}{Style.RESET_ALL}")
- except Exception as e:
- if translator:
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('config.backup_failed', error=str(e)) if translator else f'Failed to backup config: {str(e)}'}{Style.RESET_ALL}")
- else:
- if translator:
- print(f"\n{Fore.CYAN}{EMOJI['INFO']} {translator.get('config.config_force_update_disabled', fallback='Config file force update disabled by configuration. Keeping existing config file.') if translator else 'Config file force update disabled by configuration. Keeping existing config file.'}{Style.RESET_ALL}")
-
- # Generate a new (or updated) configuration if needed
- return setup_config(translator)
-
+ timestamp = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
+ backup_file = f"{config_file}.{timestamp}.bak"
+
+ import shutil
+ shutil.copy2(config_file, backup_file)
+
+ msg = translator.get('config.backup_created', fallback='Backup created: {path}', path=backup_file) if translator else f"Backup created: {backup_file}"
+ logger.info(f"Config backup created: {backup_file}")
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {msg}{Style.RESET_ALL}")
except Exception as e:
- if translator:
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('config.force_update_failed', error=str(e)) if translator else f'Force update config failed: {str(e)}'}{Style.RESET_ALL}")
- return None
+ logger.error(f"Error creating config backup: {e}")
+
+ # Setup new config
+ return setup_config(translator)
-def get_config(translator=None):
- """Get existing config or create new one"""
+def get_config(translator: Any = None) -> configparser.ConfigParser:
+ """Get configuration
+
+ Args:
+ translator: Optional translator for internationalization
+
+ Returns:
+ configparser.ConfigParser: ConfigParser object
+ """
global _config_cache
- if _config_cache is None:
- _config_cache = setup_config(translator)
- return _config_cache
\ No newline at end of file
+
+ if _config_cache is not None:
+ return _config_cache
+
+ try:
+ config_dir = os.path.join(get_user_documents_path(), ".cursor-free-vip")
+ config_file = os.path.join(config_dir, "config.ini")
+
+ if os.path.exists(config_file):
+ config = configparser.ConfigParser()
+ config.read(config_file)
+ _config_cache = config
+ return config
+ else:
+ _config_cache = setup_config(translator)
+ return _config_cache
+ except Exception as e:
+ logger.error(f"Error getting configuration: {e}")
+ return configparser.ConfigParser()
\ No newline at end of file
diff --git a/enhanced_config.py b/enhanced_config.py
new file mode 100644
index 0000000..51082b7
--- /dev/null
+++ b/enhanced_config.py
@@ -0,0 +1,475 @@
+
+
+import os
+import sys
+import configparser
+import logging
+import tempfile
+import datetime
+import platform
+import json
+import shutil
+from pathlib import Path
+from typing import Optional, Dict, Any, Union, List, Tuple, TypeVar, Generic
+from dataclasses import dataclass, field
+from enum import Enum
+from colorama import Fore, Style, init
+import yaml
+
+# Initialize colorama
+init(autoreset=True)
+
+# Configure enhanced logging
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S",
+ handlers=[
+ logging.FileHandler("cursor_free_vip.log"),
+ logging.StreamHandler()
+ ]
+)
+logger = logging.getLogger(__name__)
+
+# Enhanced emoji constants
+EMOJI = {
+ "INFO": "โน๏ธ",
+ "WARNING": "โ ๏ธ",
+ "ERROR": "โ",
+ "SUCCESS": "โ
",
+ "ADMIN": "๐",
+ "ARROW": "โก๏ธ",
+ "USER": "๐ค",
+ "KEY": "๐",
+ "SETTINGS": "โ๏ธ",
+ "CONFIG": "๐",
+ "VALIDATION": "๐",
+ "BACKUP": "๐พ",
+ "RESTORE": "๐",
+ "SECURITY": "๐",
+ "PERFORMANCE": "โก"
+}
+
+class ConfigFormat(Enum):
+ INI = "ini"
+ JSON = "json"
+ YAML = "yaml"
+
+class ValidationError(Exception):
+ pass
+
+@dataclass
+class BrowserConfig:
+ default_browser: str = "chrome"
+ chrome_path: str = ""
+ chrome_driver_path: str = ""
+ edge_path: str = ""
+ edge_driver_path: str = ""
+ firefox_path: str = ""
+ firefox_driver_path: str = ""
+ brave_path: str = ""
+ brave_driver_path: str = ""
+ opera_path: str = ""
+ opera_driver_path: str = ""
+ operagx_path: str = ""
+ operagx_driver_path: str = ""
+
+@dataclass
+class TimingConfig:
+ min_random_time: float = 0.1
+ max_random_time: float = 0.8
+ page_load_wait: str = "0.1-0.8"
+ input_wait: str = "0.3-0.8"
+ submit_wait: str = "0.5-1.5"
+ verification_code_input: str = "0.1-0.3"
+ verification_success_wait: str = "2-3"
+ verification_retry_wait: str = "2-3"
+ email_check_initial_wait: str = "4-6"
+ email_refresh_wait: str = "2-4"
+ settings_page_load_wait: str = "1-2"
+ failed_retry_time: str = "0.5-1"
+ retry_interval: str = "8-12"
+ max_timeout: int = 160
+
+@dataclass
+class SecurityConfig:
+ enable_encryption: bool = True
+ encryption_key: str = ""
+ enable_backup: bool = True
+ backup_retention_days: int = 30
+ enable_audit_log: bool = True
+ sensitive_fields: List[str] = field(default_factory=lambda: ["password", "token", "key"])
+
+class EnhancedConfigManager:
+
+ def __init__(self, translator: Any = None, config_format: ConfigFormat = ConfigFormat.INI):
+ self.translator = translator
+ self.config_format = config_format
+ self.config_dir = None
+ self.config_file = None
+ self.backup_dir = None
+ self.audit_log_file = None
+ self._config_cache = {}
+ self._validation_schema = self._load_validation_schema()
+
+ def _get_message(self, key: str, fallback: str, **kwargs) -> str:
+ """Get translated message or fallback with enhanced error handling"""
+ try:
+ if self.translator:
+ return self.translator.get(key, fallback=fallback, **kwargs)
+ except Exception as e:
+ logger.warning(f"Translation error for key '{key}': {e}")
+ return fallback.format(**kwargs) if kwargs else fallback
+
+ def _load_validation_schema(self) -> Dict[str, Any]:
+ """Load configuration validation schema"""
+ return {
+ "Browser": {
+ "default_browser": {"type": "str", "allowed": ["chrome", "edge", "firefox", "brave", "opera", "operagx"]},
+ "chrome_path": {"type": "str", "required": False},
+ "chrome_driver_path": {"type": "str", "required": False}
+ },
+ "Timing": {
+ "min_random_time": {"type": "float", "min": 0.0, "max": 10.0},
+ "max_random_time": {"type": "float", "min": 0.0, "max": 10.0},
+ "max_timeout": {"type": "int", "min": 10, "max": 600}
+ },
+ "Security": {
+ "enable_encryption": {"type": "bool"},
+ "backup_retention_days": {"type": "int", "min": 1, "max": 365}
+ }
+ }
+
+ def setup_config_directory(self) -> Tuple[str, str]:
+ """Setup configuration directory with enhanced error handling"""
+ try:
+ # Get documents path with fallback
+ docs_path = self._get_documents_path()
+ config_dir = os.path.normpath(os.path.join(docs_path, ".cursor-free-vip"))
+ config_file = os.path.normpath(os.path.join(config_dir, self._get_config_filename()))
+
+ # Create directory structure
+ self._create_directory_structure(config_dir)
+
+ self.config_dir = config_dir
+ self.config_file = config_file
+ self.backup_dir = os.path.join(config_dir, "backups")
+ self.audit_log_file = os.path.join(config_dir, "audit.log")
+
+ return config_dir, config_file
+
+ except Exception as e:
+ logger.error(f"Failed to setup config directory: {e}")
+ raise ValidationError(f"Configuration setup failed: {e}")
+
+ def _get_documents_path(self) -> str:
+ """Get documents path with enhanced platform detection"""
+ try:
+ if platform.system() == "Windows":
+ import winreg
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER,
+ "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders") as key:
+ documents_path, _ = winreg.QueryValueEx(key, "Personal")
+ return documents_path
+ elif platform.system() == "Darwin": # macOS
+ return os.path.expanduser("~/Documents")
+ else: # Linux
+ # Try XDG user directories
+ xdg_config = os.path.expanduser("~/.config/user-dirs.dirs")
+ if os.path.exists(xdg_config):
+ with open(xdg_config, "r") as f:
+ for line in f:
+ if line.startswith("XDG_DOCUMENTS_DIR"):
+ path = line.split("=")[1].strip().strip('"').replace("$HOME", os.path.expanduser("~"))
+ if os.path.exists(path):
+ return path
+ return os.path.expanduser("~/Documents")
+ except Exception as e:
+ logger.warning(f"Failed to get documents path: {e}")
+ return os.path.abspath('.')
+
+ def _get_config_filename(self) -> str:
+ """Get configuration filename based on format"""
+ format_extensions = {
+ ConfigFormat.INI: "config.ini",
+ ConfigFormat.JSON: "config.json",
+ ConfigFormat.YAML: "config.yaml"
+ }
+ return format_extensions.get(self.config_format, "config.ini")
+
+ def _create_directory_structure(self, config_dir: str) -> None:
+ """Create directory structure with proper permissions"""
+ try:
+ os.makedirs(config_dir, exist_ok=True)
+
+ # Create subdirectories
+ subdirs = ["backups", "logs", "cache", "temp"]
+ for subdir in subdirs:
+ subdir_path = os.path.join(config_dir, subdir)
+ os.makedirs(subdir_path, exist_ok=True)
+
+ # Set proper permissions on Unix systems
+ if platform.system() != "Windows":
+ os.chmod(config_dir, 0o700)
+
+ except Exception as e:
+ logger.error(f"Failed to create directory structure: {e}")
+ raise
+
+ def validate_config(self, config_data: Dict[str, Any]) -> List[str]:
+ """Validate configuration data against schema"""
+ errors = []
+
+ for section, section_schema in self._validation_schema.items():
+ if section not in config_data:
+ continue
+
+ section_data = config_data[section]
+ for key, validation in section_schema.items():
+ if key not in section_data:
+ if validation.get("required", False):
+ errors.append(f"Missing required field: {section}.{key}")
+ continue
+
+ value = section_data[key]
+ value_type = validation.get("type")
+
+ # Type validation
+ if value_type == "str" and not isinstance(value, str):
+ errors.append(f"Invalid type for {section}.{key}: expected str, got {type(value)}")
+ elif value_type == "int" and not isinstance(value, int):
+ errors.append(f"Invalid type for {section}.{key}: expected int, got {type(value)}")
+ elif value_type == "float" and not isinstance(value, (int, float)):
+ errors.append(f"Invalid type for {section}.{key}: expected float, got {type(value)}")
+ elif value_type == "bool" and not isinstance(value, bool):
+ errors.append(f"Invalid type for {section}.{key}: expected bool, got {type(value)}")
+
+ # Range validation
+ if "min" in validation and value < validation["min"]:
+ errors.append(f"Value too small for {section}.{key}: {value} < {validation['min']}")
+ if "max" in validation and value > validation["max"]:
+ errors.append(f"Value too large for {section}.{key}: {value} > {validation['max']}")
+
+ # Allowed values validation
+ if "allowed" in validation and value not in validation["allowed"]:
+ errors.append(f"Invalid value for {section}.{key}: {value} not in {validation['allowed']}")
+
+ return errors
+
+ def get_default_config(self) -> Dict[str, Any]:
+ """Get enhanced default configuration"""
+ from utils import get_default_browser_path, get_default_driver_path
+
+ config_dir = self.config_dir or os.path.join(self._get_documents_path(), ".cursor-free-vip")
+
+ default_config = {
+ 'Browser': {
+ 'default_browser': 'chrome',
+ 'chrome_path': get_default_browser_path('chrome'),
+ 'chrome_driver_path': get_default_driver_path('chrome'),
+ 'edge_path': get_default_browser_path('edge'),
+ 'edge_driver_path': get_default_driver_path('edge'),
+ 'firefox_path': get_default_browser_path('firefox'),
+ 'firefox_driver_path': get_default_driver_path('firefox'),
+ 'brave_path': get_default_browser_path('brave'),
+ 'brave_driver_path': get_default_driver_path('brave'),
+ 'opera_path': get_default_browser_path('opera'),
+ 'opera_driver_path': get_default_driver_path('opera'),
+ 'operagx_path': get_default_browser_path('operagx'),
+ 'operagx_driver_path': get_default_driver_path('chrome')
+ },
+ 'Timing': {
+ 'min_random_time': 0.1,
+ 'max_random_time': 0.8,
+ 'page_load_wait': '0.1-0.8',
+ 'input_wait': '0.3-0.8',
+ 'submit_wait': '0.5-1.5',
+ 'verification_code_input': '0.1-0.3',
+ 'verification_success_wait': '2-3',
+ 'verification_retry_wait': '2-3',
+ 'email_check_initial_wait': '4-6',
+ 'email_refresh_wait': '2-4',
+ 'settings_page_load_wait': '1-2',
+ 'failed_retry_time': '0.5-1',
+ 'retry_interval': '8-12',
+ 'max_timeout': 160
+ },
+ 'Security': {
+ 'enable_encryption': True,
+ 'encryption_key': '',
+ 'enable_backup': True,
+ 'backup_retention_days': 30,
+ 'enable_audit_log': True,
+ 'sensitive_fields': ['password', 'token', 'key', 'secret']
+ },
+ 'Performance': {
+ 'enable_caching': True,
+ 'cache_ttl': 3600,
+ 'max_concurrent_operations': 5,
+ 'enable_compression': True
+ },
+ 'Logging': {
+ 'log_level': 'INFO',
+ 'log_file': 'cursor_free_vip.log',
+ 'max_log_size': 10485760, # 10MB
+ 'log_rotation': 5
+ }
+ }
+
+ # Add system-specific paths
+ self._add_system_paths(default_config)
+
+ return default_config
+
+ def _add_system_paths(self, config: Dict[str, Any]) -> None:
+ system = platform.system()
+
+ if system == "Windows":
+ self._add_windows_paths(config)
+ elif system == "Darwin":
+ self._add_macos_paths(config)
+ else:
+ self._add_linux_paths(config)
+
+ def _add_windows_paths(self, config: Dict[str, Any]) -> None:
+ username = os.getenv('USERNAME', 'user')
+ config['WindowsPaths'] = {
+ 'storage_path': f"C:\\Users\\{username}\\AppData\\Roaming\\Cursor\\User\\globalStorage\\storage.json",
+ 'sqlite_path': f"C:\\Users\\{username}\\AppData\\Roaming\\Cursor\\User\\globalStorage\\state.vscdb",
+ 'machine_id_path': f"C:\\Users\\{username}\\AppData\\Roaming\\Cursor\\machineId",
+ 'cursor_path': f"C:\\Users\\{username}\\AppData\\Local\\Programs\\Cursor\\resources\\app",
+ 'updater_path': f"C:\\Users\\{username}\\AppData\\Local\\cursor-updater",
+ 'update_yml_path': f"C:\\Users\\{username}\\AppData\\Local\\Programs\\Cursor\\resources\\app-update.yml",
+ 'product_json_path': f"C:\\Users\\{username}\\AppData\\Local\\Programs\\Cursor\\resources\\app\\product.json"
+ }
+
+ def _add_macos_paths(self, config: Dict[str, Any]) -> None:
+ username = os.getenv('USER', 'user')
+ config['MacOSPaths'] = {
+ 'storage_path': f"/Users/{username}/Library/Application Support/Cursor/User/globalStorage/storage.json",
+ 'sqlite_path': f"/Users/{username}/Library/Application Support/Cursor/User/globalStorage/state.vscdb",
+ 'machine_id_path': f"/Users/{username}/Library/Application Support/Cursor/machineId",
+ 'cursor_path': f"/Applications/Cursor.app/Contents/Resources/app",
+ 'updater_path': f"/Users/{username}/Library/Application Support/cursor-updater",
+ 'update_yml_path': f"/Applications/Cursor.app/Contents/Resources/app-update.yml",
+ 'product_json_path': f"/Applications/Cursor.app/Contents/Resources/app/product.json"
+ }
+
+ def _add_linux_paths(self, config: Dict[str, Any]) -> None:
+ username = os.getenv('USER', 'user')
+ config['LinuxPaths'] = {
+ 'storage_path': f"/home/{username}/.config/Cursor/User/globalStorage/storage.json",
+ 'sqlite_path': f"/home/{username}/.config/Cursor/User/globalStorage/state.vscdb",
+ 'machine_id_path': f"/home/{username}/.config/Cursor/machineid",
+ 'cursor_path': "/opt/Cursor/resources/app",
+ 'updater_path': f"/home/{username}/.config/cursor-updater",
+ 'update_yml_path': "/opt/Cursor/resources/app-update.yml",
+ 'product_json_path': "/opt/Cursor/resources/app/product.json"
+ }
+
+ def save_config(self, config_data: Dict[str, Any], backup: bool = True) -> None:
+ try:
+ errors = self.validate_config(config_data)
+ if errors:
+ raise ValidationError(f"Configuration validation failed:\n" + "\n".join(errors))
+
+ if backup and self.backup_dir:
+ self._create_backup()
+
+ if self.config_format == ConfigFormat.JSON:
+ self._save_json_config(config_data)
+ elif self.config_format == ConfigFormat.YAML:
+ self._save_yaml_config(config_data)
+ else:
+ self._save_ini_config(config_data)
+
+ self._log_config_change("Configuration saved successfully")
+
+ except Exception as e:
+ logger.error(f"Failed to save configuration: {e}")
+ raise
+
+ def _create_backup(self) -> None:
+ try:
+ if not self.config_file or not os.path.exists(self.config_file):
+ return
+
+ if not self.backup_dir:
+ return
+
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
+ backup_filename = f"config_backup_{timestamp}.{self.config_format.value}"
+ backup_path = os.path.join(self.backup_dir, backup_filename)
+
+ shutil.copy2(self.config_file, backup_path)
+
+ self._cleanup_old_backups()
+
+ logger.info(f"Configuration backup created: {backup_path}")
+
+ except Exception as e:
+ logger.warning(f"Failed to create backup: {e}")
+
+ def _cleanup_old_backups(self) -> None:
+ try:
+ if not self.backup_dir:
+ return
+
+ retention_days = 30
+ cutoff_time = datetime.datetime.now() - datetime.timedelta(days=retention_days)
+
+ for filename in os.listdir(self.backup_dir):
+ if filename.startswith("config_backup_"):
+ file_path = os.path.join(self.backup_dir, filename)
+ file_time = datetime.datetime.fromtimestamp(os.path.getctime(file_path))
+
+ if file_time < cutoff_time:
+ os.remove(file_path)
+ logger.info(f"Removed old backup: {filename}")
+
+ except Exception as e:
+ logger.warning(f"Failed to cleanup old backups: {e}")
+
+ def _save_json_config(self, config_data: Dict[str, Any]) -> None:
+ if self.config_file:
+ with open(self.config_file, 'w', encoding='utf-8') as f:
+ json.dump(config_data, f, indent=2, ensure_ascii=False)
+
+ def _save_yaml_config(self, config_data: Dict[str, Any]) -> None:
+ if self.config_file:
+ with open(self.config_file, 'w', encoding='utf-8') as f:
+ yaml.dump(config_data, f, default_flow_style=False, allow_unicode=True)
+
+ def _save_ini_config(self, config_data: Dict[str, Any]) -> None:
+ config = configparser.ConfigParser()
+
+ for section, items in config_data.items():
+ config.add_section(section)
+ for key, value in items.items():
+ config.set(section, key, str(value))
+
+ if self.config_file:
+ with open(self.config_file, 'w', encoding='utf-8') as f:
+ config.write(f)
+
+ def _log_config_change(self, message: str) -> None:
+ try:
+ if self.audit_log_file:
+ timestamp = datetime.datetime.now().isoformat()
+ log_entry = f"{timestamp} - {message}\n"
+
+ with open(self.audit_log_file, 'a', encoding='utf-8') as f:
+ f.write(log_entry)
+
+ except Exception as e:
+ logger.warning(f"Failed to log configuration change: {e}")
+
+def create_config_manager(translator: Any = None, format_type: str = "ini") -> EnhancedConfigManager:
+ format_map = {
+ "ini": ConfigFormat.INI,
+ "json": ConfigFormat.JSON,
+ "yaml": ConfigFormat.YAML
+ }
+
+ config_format = format_map.get(format_type.lower(), ConfigFormat.INI)
+ return EnhancedConfigManager(translator, config_format)
\ No newline at end of file
diff --git a/enhanced_error_handler.py b/enhanced_error_handler.py
new file mode 100644
index 0000000..9c33f5e
--- /dev/null
+++ b/enhanced_error_handler.py
@@ -0,0 +1,351 @@
+
+
+import os
+import sys
+import logging
+import traceback
+import json
+import time
+from typing import Optional, Dict, Any, Callable, List, Union
+from dataclasses import dataclass, field
+from enum import Enum
+from pathlib import Path
+import threading
+from contextlib import contextmanager
+
+class ErrorSeverity(Enum):
+ LOW = "low"
+ MEDIUM = "medium"
+ HIGH = "high"
+ CRITICAL = "critical"
+
+class ErrorCategory(Enum):
+ CONFIGURATION = "configuration"
+ NETWORK = "network"
+ PROCESS = "process"
+ FILE_SYSTEM = "file_system"
+ PERMISSION = "permission"
+ VALIDATION = "validation"
+ BROWSER = "browser"
+ AUTHENTICATION = "authentication"
+ SYSTEM = "system"
+ UNKNOWN = "unknown"
+
+@dataclass
+class ErrorInfo:
+ timestamp: float
+ category: ErrorCategory
+ severity: ErrorSeverity
+ message: str
+ exception: Optional[Exception] = None
+ traceback: Optional[str] = None
+ context: Dict[str, Any] = field(default_factory=dict)
+ user_action: Optional[str] = None
+ resolved: bool = False
+ resolution_time: Optional[float] = None
+
+class ErrorRecoveryStrategy:
+
+ def __init__(self, max_retries: int = 3, backoff_factor: float = 2.0):
+ self.max_retries = max_retries
+ self.backoff_factor = backoff_factor
+ self.retry_counts = {}
+
+ def should_retry(self, error_info: ErrorInfo) -> bool:
+ """Determine if operation should be retried"""
+ error_key = f"{error_info.category.value}_{error_info.message}"
+ current_retries = self.retry_counts.get(error_key, 0)
+
+ if current_retries >= self.max_retries:
+ return False
+
+ # Don't retry critical errors
+ if error_info.severity == ErrorSeverity.CRITICAL:
+ return False
+
+ # Don't retry permission errors
+ if error_info.category == ErrorCategory.PERMISSION:
+ return False
+
+ return True
+
+ def get_retry_delay(self, error_info: ErrorInfo) -> float:
+ """Get delay before next retry"""
+ error_key = f"{error_info.category.value}_{error_info.message}"
+ current_retries = self.retry_counts.get(error_key, 0)
+ return (self.backoff_factor ** current_retries)
+
+ def record_retry(self, error_info: ErrorInfo) -> None:
+ """Record a retry attempt"""
+ error_key = f"{error_info.category.value}_{error_info.message}"
+ self.retry_counts[error_key] = self.retry_counts.get(error_key, 0) + 1
+
+class EnhancedErrorHandler:
+ """Enhanced error handler with logging, categorization, and recovery"""
+
+ def __init__(self, log_file: Optional[str] = None, enable_recovery: bool = True):
+ self.log_file = log_file or "cursor_free_vip_errors.log"
+ self.enable_recovery = enable_recovery
+ self.recovery_strategy = ErrorRecoveryStrategy()
+ self.error_history: List[ErrorInfo] = []
+ self.error_callbacks: Dict[ErrorCategory, List[Callable]] = {}
+ self._setup_logging()
+ self._lock = threading.Lock()
+
+ def _setup_logging(self) -> None:
+ """Setup enhanced logging"""
+ # Create logs directory if it doesn't exist
+ log_dir = os.path.dirname(self.log_file)
+ if log_dir:
+ os.makedirs(log_dir, exist_ok=True)
+
+ # Configure file handler
+ file_handler = logging.FileHandler(self.log_file, encoding='utf-8')
+ file_handler.setLevel(logging.DEBUG)
+
+ # Configure console handler
+ console_handler = logging.StreamHandler()
+ console_handler.setLevel(logging.INFO)
+
+ # Create formatter
+ formatter = logging.Formatter(
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ datefmt='%Y-%m-%d %H:%M:%S'
+ )
+ file_handler.setFormatter(formatter)
+ console_handler.setFormatter(formatter)
+
+ # Configure logger
+ self.logger = logging.getLogger('CursorFreeVIP.ErrorHandler')
+ self.logger.setLevel(logging.DEBUG)
+ self.logger.addHandler(file_handler)
+ self.logger.addHandler(console_handler)
+
+ def categorize_error(self, exception: Exception, context: Optional[Dict[str, Any]] = None) -> ErrorCategory:
+ """Categorize error based on exception type and context"""
+ exception_type = type(exception).__name__
+ exception_message = str(exception).lower()
+
+ # Network errors
+ if any(keyword in exception_message for keyword in ['connection', 'timeout', 'network', 'http', 'url']):
+ return ErrorCategory.NETWORK
+
+ # File system errors
+ if any(keyword in exception_message for keyword in ['file', 'directory', 'path', 'not found', 'permission']):
+ if 'permission' in exception_message:
+ return ErrorCategory.PERMISSION
+ return ErrorCategory.FILE_SYSTEM
+
+ # Process errors
+ if any(keyword in exception_message for keyword in ['process', 'pid', 'terminate', 'kill']):
+ return ErrorCategory.PROCESS
+
+ # Browser errors
+ if any(keyword in exception_message for keyword in ['browser', 'driver', 'selenium', 'chrome', 'firefox']):
+ return ErrorCategory.BROWSER
+
+ # Authentication errors
+ if any(keyword in exception_message for keyword in ['auth', 'login', 'token', 'credential']):
+ return ErrorCategory.AUTHENTICATION
+
+ # Configuration errors
+ if any(keyword in exception_message for keyword in ['config', 'setting', 'parameter']):
+ return ErrorCategory.CONFIGURATION
+
+ # Validation errors
+ if any(keyword in exception_message for keyword in ['validation', 'invalid', 'format']):
+ return ErrorCategory.VALIDATION
+
+ # System errors
+ if any(keyword in exception_message for keyword in ['system', 'os', 'platform']):
+ return ErrorCategory.SYSTEM
+
+ return ErrorCategory.UNKNOWN
+
+ def determine_severity(self, exception: Exception, category: ErrorCategory) -> ErrorSeverity:
+ """Determine error severity"""
+ exception_message = str(exception).lower()
+
+ # Critical errors
+ if any(keyword in exception_message for keyword in ['fatal', 'critical', 'corrupt', 'broken']):
+ return ErrorSeverity.CRITICAL
+
+ # High severity errors
+ if category in [ErrorCategory.PERMISSION, ErrorCategory.AUTHENTICATION]:
+ return ErrorSeverity.HIGH
+
+ if any(keyword in exception_message for keyword in ['access denied', 'unauthorized', 'forbidden']):
+ return ErrorSeverity.HIGH
+
+ # Medium severity errors
+ if category in [ErrorCategory.NETWORK, ErrorCategory.FILE_SYSTEM, ErrorCategory.PROCESS]:
+ return ErrorSeverity.MEDIUM
+
+ # Low severity errors
+ if category in [ErrorCategory.VALIDATION, ErrorCategory.CONFIGURATION]:
+ return ErrorSeverity.LOW
+
+ return ErrorSeverity.MEDIUM
+
+ def handle_error(self, exception: Exception, context: Optional[Dict[str, Any]] = None,
+ user_action: Optional[str] = None) -> ErrorInfo:
+ with self._lock:
+ category = self.categorize_error(exception, context)
+ severity = self.determine_severity(exception, category)
+ error_info = ErrorInfo(
+ timestamp=time.time(),
+ category=category,
+ severity=severity,
+ message=str(exception),
+ exception=exception,
+ traceback=traceback.format_exc(),
+ context=context or {},
+ user_action=user_action
+ )
+
+ self._log_error(error_info)
+
+ self.error_history.append(error_info)
+
+ self._execute_callbacks(error_info)
+
+ if self.enable_recovery:
+ self._attempt_recovery(error_info)
+
+ return error_info
+
+ def _log_error(self, error_info: ErrorInfo) -> None:
+ log_message = f"""
+Error Details:
+- Category: {error_info.category.value}
+- Severity: {error_info.severity.value}
+- Message: {error_info.message}
+- Timestamp: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(error_info.timestamp))}
+- User Action: {error_info.user_action or 'N/A'}
+- Context: {json.dumps(error_info.context, indent=2)}
+"""
+
+ if error_info.traceback:
+ log_message += f"\nTraceback:\n{error_info.traceback}"
+
+ if error_info.severity == ErrorSeverity.CRITICAL:
+ self.logger.critical(log_message)
+ elif error_info.severity == ErrorSeverity.HIGH:
+ self.logger.error(log_message)
+ elif error_info.severity == ErrorSeverity.MEDIUM:
+ self.logger.warning(log_message)
+ else:
+ self.logger.info(log_message)
+
+ def _execute_callbacks(self, error_info: ErrorInfo) -> None:
+ callbacks = self.error_callbacks.get(error_info.category, [])
+ for callback in callbacks:
+ try:
+ callback(error_info)
+ except Exception as e:
+ self.logger.error(f"Error in callback: {e}")
+
+ def _attempt_recovery(self, error_info: ErrorInfo) -> None:
+ if not self.recovery_strategy.should_retry(error_info):
+ return
+
+ delay = self.recovery_strategy.get_retry_delay(error_info)
+ self.logger.info(f"Attempting recovery in {delay:.2f} seconds...")
+
+ self.recovery_strategy.record_retry(error_info)
+
+ threading.Timer(delay, self._retry_operation, args=[error_info]).start()
+
+ def _retry_operation(self, error_info: ErrorInfo) -> None:
+ self.logger.info(f"Retrying operation for error: {error_info.message}")
+
+ def register_callback(self, category: ErrorCategory, callback: Callable[[ErrorInfo], None]) -> None:
+ if category not in self.error_callbacks:
+ self.error_callbacks[category] = []
+ self.error_callbacks[category].append(callback)
+
+ def get_error_summary(self, hours: int = 24) -> Dict[str, Any]:
+ cutoff_time = time.time() - (hours * 3600)
+ recent_errors = [e for e in self.error_history if e.timestamp >= cutoff_time]
+
+ summary = {
+ 'total_errors': len(recent_errors),
+ 'by_category': {},
+ 'by_severity': {},
+ 'most_common': [],
+ 'unresolved': len([e for e in recent_errors if not e.resolved])
+ }
+
+ for error in recent_errors:
+ category = error.category.value
+ severity = error.severity.value
+
+ summary['by_category'][category] = summary['by_category'].get(category, 0) + 1
+ summary['by_severity'][severity] = summary['by_severity'].get(severity, 0) + 1
+
+ error_messages = [e.message for e in recent_errors]
+ from collections import Counter
+ message_counts = Counter(error_messages)
+ summary['most_common'] = message_counts.most_common(5)
+
+ return summary
+
+ def resolve_error(self, error_info: ErrorInfo, resolution: str) -> None:
+ error_info.resolved = True
+ error_info.resolution_time = time.time()
+ error_info.user_action = resolution
+
+ self.logger.info(f"Error resolved: {error_info.message} - Resolution: {resolution}")
+
+ def clear_history(self, older_than_hours: int = 168) -> None:
+ cutoff_time = time.time() - (older_than_hours * 3600)
+ with self._lock:
+ self.error_history = [e for e in self.error_history if e.timestamp >= cutoff_time]
+
+ self.logger.info(f"Cleared error history older than {older_than_hours} hours")
+
+@contextmanager
+def error_context(handler: EnhancedErrorHandler, context: Optional[Dict[str, Any]] = None,
+ user_action: Optional[str] = None):
+ try:
+ yield
+ except Exception as e:
+ handler.handle_error(e, context, user_action)
+ raise
+
+error_handler = EnhancedErrorHandler()
+
+def handle_error(exception: Exception, context: Optional[Dict[str, Any]] = None,
+ user_action: Optional[str] = None) -> ErrorInfo:
+ return error_handler.handle_error(exception, context, user_action)
+
+def safe_execute(func: Callable, *args, context: Optional[Dict[str, Any]] = None,
+ user_action: Optional[str] = None, **kwargs) -> Any:
+ try:
+ return func(*args, **kwargs)
+ except Exception as e:
+ error_handler.handle_error(e, context, user_action)
+ raise
+
+def retry_on_error(func: Callable, max_retries: int = 3, *args,
+ context: Optional[Dict[str, Any]] = None, user_action: Optional[str] = None, **kwargs) -> Any:
+ last_exception: Optional[Exception] = None
+
+ for attempt in range(max_retries):
+ try:
+ return func(*args, **kwargs)
+ except Exception as e:
+ last_exception = e
+ error_info = error_handler.handle_error(e, context, user_action)
+
+ if attempt < max_retries - 1:
+ delay = 2 ** attempt
+ time.sleep(delay)
+ continue
+ else:
+ break
+
+ if last_exception is not None:
+ raise last_exception
+ else:
+ raise RuntimeError("Unexpected error in retry_on_error")
\ No newline at end of file
diff --git a/enhanced_utils.py b/enhanced_utils.py
new file mode 100644
index 0000000..aef6320
--- /dev/null
+++ b/enhanced_utils.py
@@ -0,0 +1,677 @@
+
+
+import os
+import sys
+import platform
+import random
+import shutil
+import logging
+import subprocess
+import threading
+import time
+import hashlib
+import json
+from typing import Optional, Dict, List, Union, Tuple, Any, Callable
+from pathlib import Path
+from dataclasses import dataclass
+from enum import Enum
+import psutil
+import requests
+from concurrent.futures import ThreadPoolExecutor, as_completed
+
+# Configure enhanced logging
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S"
+)
+logger = logging.getLogger(__name__)
+
+class ProcessStatus(Enum):
+ RUNNING = "running"
+ STOPPED = "stopped"
+ NOT_FOUND = "not_found"
+ ERROR = "error"
+
+@dataclass
+class ProcessInfo:
+ pid: int
+ name: str
+ status: ProcessStatus
+ memory_usage: float = 0.0
+ cpu_usage: float = 0.0
+ start_time: float = 0.0
+ command_line: str = ""
+
+@dataclass
+class SystemInfo:
+ platform: str
+ architecture: str
+ python_version: str
+ total_memory: float
+ available_memory: float
+ cpu_count: int
+ disk_usage: Dict[str, float]
+
+class EnhancedPathManager:
+
+ def __init__(self):
+ self._path_cache = {}
+ self._executable_cache = {}
+ self._browser_cache = {}
+
+ def get_user_documents_path(self) -> str:
+ """Get user documents path with enhanced error handling"""
+ cache_key = "documents_path"
+ if cache_key in self._path_cache:
+ return self._path_cache[cache_key]
+
+ try:
+ if platform.system() == "Windows":
+ path = self._get_windows_documents_path()
+ elif platform.system() == "Darwin":
+ path = self._get_macos_documents_path()
+ else:
+ path = self._get_linux_documents_path()
+
+ # Validate path exists
+ if not os.path.exists(path):
+ logger.warning(f"Documents path does not exist: {path}")
+ path = os.path.expanduser("~/Documents")
+
+ self._path_cache[cache_key] = path
+ return path
+
+ except Exception as e:
+ logger.error(f"Failed to get documents path: {e}")
+ fallback = os.path.expanduser("~/Documents")
+ self._path_cache[cache_key] = fallback
+ return fallback
+
+ def _get_windows_documents_path(self) -> str:
+ """Get Windows documents path from registry"""
+ try:
+ import winreg
+ with winreg.OpenKey(winreg.HKEY_CURRENT_USER,
+ "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders") as key:
+ documents_path, _ = winreg.QueryValueEx(key, "Personal")
+ return documents_path
+ except Exception as e:
+ logger.warning(f"Failed to get Windows documents path from registry: {e}")
+ return os.path.expanduser("~\\Documents")
+
+ def _get_macos_documents_path(self) -> str:
+ """Get macOS documents path"""
+ return os.path.expanduser("~/Documents")
+
+ def _get_linux_documents_path(self) -> str:
+ """Get Linux documents path with XDG support"""
+ try:
+ xdg_config = os.path.expanduser("~/.config/user-dirs.dirs")
+ if os.path.exists(xdg_config):
+ with open(xdg_config, "r") as f:
+ for line in f:
+ if line.startswith("XDG_DOCUMENTS_DIR"):
+ path = line.split("=")[1].strip().strip('"').replace("$HOME", os.path.expanduser("~"))
+ if os.path.exists(path):
+ return path
+ except Exception as e:
+ logger.warning(f"Failed to read XDG config: {e}")
+
+ return os.path.expanduser("~/Documents")
+
+ def find_executable(self, executable_names: List[str], validate: bool = True) -> Optional[str]:
+ """Find executable with enhanced validation"""
+ cache_key = tuple(sorted(executable_names))
+ if cache_key in self._executable_cache:
+ return self._executable_cache[cache_key]
+
+ for name in executable_names:
+ try:
+ path = shutil.which(name)
+ if path and (not validate or self._validate_executable(path)):
+ self._executable_cache[cache_key] = path
+ return path
+ except Exception as e:
+ logger.debug(f"Failed to find executable {name}: {e}")
+ continue
+
+ self._executable_cache[cache_key] = None
+ return None
+
+ def _validate_executable(self, path: str) -> bool:
+ """Validate executable file"""
+ try:
+ if not os.path.exists(path):
+ return False
+
+ # Check if file is executable
+ if platform.system() != "Windows":
+ if not os.access(path, os.X_OK):
+ return False
+
+ # Check file size (should not be 0)
+ if os.path.getsize(path) == 0:
+ return False
+
+ return True
+
+ except Exception as e:
+ logger.debug(f"Executable validation failed for {path}: {e}")
+ return False
+
+class EnhancedBrowserManager:
+ """Enhanced browser management with automatic detection and validation"""
+
+ def __init__(self):
+ self.path_manager = EnhancedPathManager()
+ self._browser_paths = {}
+ self._driver_paths = {}
+
+ def get_browser_path(self, browser_type: str) -> str:
+ """Get browser path with enhanced detection"""
+ browser_type = browser_type.lower()
+
+ if browser_type in self._browser_paths:
+ return self._browser_paths[browser_type]
+
+ try:
+ if platform.system() == "Windows":
+ path = self._get_windows_browser_path(browser_type)
+ elif platform.system() == "Darwin":
+ path = self._get_macos_browser_path(browser_type)
+ else:
+ path = self._get_linux_browser_path(browser_type)
+
+ self._browser_paths[browser_type] = path
+ return path
+
+ except Exception as e:
+ logger.error(f"Failed to get browser path for {browser_type}: {e}")
+ return ""
+
+ def get_driver_path(self, browser_type: str) -> str:
+ """Get driver path with enhanced detection"""
+ browser_type = browser_type.lower()
+
+ if browser_type in self._driver_paths:
+ return self._driver_paths[browser_type]
+
+ try:
+ # Map browser types to driver types
+ driver_map = {
+ 'chrome': 'chromedriver',
+ 'edge': 'msedgedriver',
+ 'firefox': 'geckodriver',
+ 'brave': 'chromedriver',
+ 'opera': 'chromedriver',
+ 'operagx': 'chromedriver'
+ }
+
+ driver_name = driver_map.get(browser_type, 'chromedriver')
+ path = self._find_driver_path(driver_name)
+
+ self._driver_paths[browser_type] = path
+ return path
+
+ except Exception as e:
+ logger.error(f"Failed to get driver path for {browser_type}: {e}")
+ return ""
+
+ def _get_windows_browser_path(self, browser_type: str) -> str:
+ """Get Windows browser path"""
+ browser_paths = {
+ 'chrome': [
+ shutil.which("chrome"),
+ r"C:\Program Files\Google\Chrome\Application\chrome.exe",
+ r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
+ os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Google', 'Chrome', 'Application', 'chrome.exe')
+ ],
+ 'edge': [
+ shutil.which("msedge"),
+ r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
+ r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
+ ],
+ 'firefox': [
+ shutil.which("firefox"),
+ r"C:\Program Files\Mozilla Firefox\firefox.exe",
+ r"C:\Program Files (x86)\Mozilla Firefox\firefox.exe"
+ ],
+ 'opera': [
+ shutil.which("opera"),
+ r"C:\Program Files\Opera\opera.exe",
+ r"C:\Program Files (x86)\Opera\opera.exe",
+ os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera', 'launcher.exe')
+ ],
+ 'operagx': [
+ os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera GX', 'launcher.exe'),
+ r"C:\Program Files\Opera GX\opera.exe",
+ r"C:\Program Files (x86)\Opera GX\opera.exe"
+ ],
+ 'brave': [
+ shutil.which("brave"),
+ os.path.join(os.environ.get('PROGRAMFILES', ''), 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
+ os.path.join(os.environ.get('PROGRAMFILES(X86)', ''), 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe')
+ ]
+ }
+
+ paths = browser_paths.get(browser_type, [])
+ for path in paths:
+ if path and os.path.exists(path):
+ return path
+
+ return ""
+
+ def _get_macos_browser_path(self, browser_type: str) -> str:
+ """Get macOS browser path"""
+ browser_paths = {
+ 'chrome': [
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
+ "~/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
+ ],
+ 'edge': [
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
+ "~/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
+ ],
+ 'firefox': [
+ "/Applications/Firefox.app/Contents/MacOS/firefox",
+ "~/Applications/Firefox.app/Contents/MacOS/firefox"
+ ],
+ 'opera': [
+ "/Applications/Opera.app/Contents/MacOS/Opera",
+ "~/Applications/Opera.app/Contents/MacOS/Opera"
+ ],
+ 'operagx': [
+ "/Applications/Opera GX.app/Contents/MacOS/Opera GX",
+ "~/Applications/Opera GX.app/Contents/MacOS/Opera GX"
+ ],
+ 'brave': [
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
+ "~/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
+ ]
+ }
+
+ paths = browser_paths.get(browser_type, [])
+ for path in paths:
+ expanded_path = os.path.expanduser(path)
+ if os.path.exists(expanded_path):
+ return expanded_path
+
+ return ""
+
+ def _get_linux_browser_path(self, browser_type: str) -> str:
+ """Get Linux browser path"""
+ browser_names = {
+ 'chrome': ['google-chrome', 'chrome', 'chromium-browser', 'chromium'],
+ 'edge': ['microsoft-edge', 'msedge', 'edge'],
+ 'firefox': ['firefox', 'firefox-esr'],
+ 'opera': ['opera', 'opera-stable'],
+ 'operagx': ['opera-gx'],
+ 'brave': ['brave-browser', 'brave']
+ }
+
+ names = browser_names.get(browser_type, [browser_type])
+ return self.path_manager.find_executable(names) or ""
+
+ def _find_driver_path(self, driver_name: str) -> str:
+ """Find driver executable path"""
+ # Try to find in PATH first
+ path = self.path_manager.find_executable([driver_name])
+ if path:
+ return path
+
+ # Try common installation paths
+ if platform.system() == "Windows":
+ driver_paths = [
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", f"{driver_name}.exe"),
+ os.path.join(os.environ.get('LOCALAPPDATA', ''), 'WebDriver', f"{driver_name}.exe"),
+ f"C:\\Program Files\\{driver_name}\\{driver_name}.exe"
+ ]
+ elif platform.system() == "Darwin":
+ driver_paths = [
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", driver_name),
+ f"/usr/local/bin/{driver_name}",
+ f"/opt/homebrew/bin/{driver_name}"
+ ]
+ else:
+ driver_paths = [
+ os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", driver_name),
+ f"/usr/local/bin/{driver_name}",
+ f"/usr/bin/{driver_name}"
+ ]
+
+ for driver_path in driver_paths:
+ if os.path.exists(driver_path):
+ return driver_path
+
+ return ""
+
+class EnhancedProcessManager:
+ """Enhanced process management with monitoring and control"""
+
+ def __init__(self):
+ self._process_cache = {}
+ self._monitoring_threads = {}
+
+ def find_cursor_processes(self) -> List[ProcessInfo]:
+ """Find all Cursor processes with detailed information"""
+ processes = []
+
+ try:
+ for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'memory_info', 'cpu_percent', 'create_time']):
+ try:
+ if self._is_cursor_process(proc.info['name']):
+ process_info = ProcessInfo(
+ pid=proc.info['pid'],
+ name=proc.info['name'],
+ status=ProcessStatus.RUNNING,
+ memory_usage=proc.info['memory_info'].rss / 1024 / 1024, # MB
+ cpu_usage=proc.info['cpu_percent'],
+ start_time=proc.info['create_time'],
+ command_line=' '.join(proc.info['cmdline']) if proc.info['cmdline'] else ''
+ )
+ processes.append(process_info)
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
+ continue
+
+ except Exception as e:
+ logger.error(f"Failed to find Cursor processes: {e}")
+
+ return processes
+
+ def _is_cursor_process(self, process_name: str) -> bool:
+ """Check if process is related to Cursor"""
+ cursor_names = ['cursor', 'Cursor', 'CURSOR']
+ return any(name in process_name for name in cursor_names)
+
+ def kill_cursor_processes(self, force: bool = False) -> bool:
+ """Kill all Cursor processes"""
+ processes = self.find_cursor_processes()
+ success = True
+
+ for process_info in processes:
+ try:
+ proc = psutil.Process(process_info.pid)
+ if force:
+ proc.kill()
+ else:
+ proc.terminate()
+
+ logger.info(f"Terminated Cursor process: {process_info.name} (PID: {process_info.pid})")
+
+ except (psutil.NoSuchProcess, psutil.AccessDenied) as e:
+ logger.warning(f"Failed to terminate process {process_info.pid}: {e}")
+ success = False
+
+ return success
+
+ def wait_for_process_termination(self, pids: List[int], timeout: int = 30) -> bool:
+ """Wait for processes to terminate"""
+ start_time = time.time()
+
+ while time.time() - start_time < timeout:
+ remaining_pids = []
+
+ for pid in pids:
+ try:
+ if psutil.pid_exists(pid):
+ remaining_pids.append(pid)
+ except Exception:
+ continue
+
+ if not remaining_pids:
+ return True
+
+ time.sleep(0.5)
+
+ logger.warning(f"Timeout waiting for process termination: {remaining_pids}")
+ return False
+
+ def monitor_process(self, pid: int, callback: Callable[[ProcessInfo], None]) -> None:
+ """Monitor process and call callback with updates"""
+ def monitor():
+ try:
+ proc = psutil.Process(pid)
+ while proc.is_running():
+ try:
+ process_info = ProcessInfo(
+ pid=proc.pid,
+ name=proc.name(),
+ status=ProcessStatus.RUNNING,
+ memory_usage=proc.memory_info().rss / 1024 / 1024,
+ cpu_usage=proc.cpu_percent(),
+ start_time=proc.create_time(),
+ command_line=' '.join(proc.cmdline()) if proc.cmdline() else ''
+ )
+ callback(process_info)
+ time.sleep(1)
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
+ break
+ except Exception as e:
+ logger.error(f"Process monitoring failed for PID {pid}: {e}")
+
+ thread = threading.Thread(target=monitor, daemon=True)
+ thread.start()
+ self._monitoring_threads[pid] = thread
+
+class EnhancedSystemManager:
+ """Enhanced system information and management"""
+
+ def __init__(self):
+ self._system_info = None
+
+ def get_system_info(self) -> SystemInfo:
+ """Get comprehensive system information"""
+ if self._system_info is None:
+ try:
+ cpu_count = psutil.cpu_count()
+ if cpu_count is None:
+ cpu_count = 1
+
+ self._system_info = SystemInfo(
+ platform=platform.system(),
+ architecture=platform.machine(),
+ python_version=sys.version,
+ total_memory=psutil.virtual_memory().total / 1024 / 1024 / 1024,
+ available_memory=psutil.virtual_memory().available / 1024 / 1024 / 1024,
+ cpu_count=cpu_count,
+ disk_usage=self._get_disk_usage()
+ )
+ except Exception as e:
+ logger.error(f"Failed to get system info: {e}")
+ # Return basic info
+ self._system_info = SystemInfo(
+ platform=platform.system(),
+ architecture=platform.machine(),
+ python_version=sys.version,
+ total_memory=0.0,
+ available_memory=0.0,
+ cpu_count=1,
+ disk_usage={}
+ )
+
+ return self._system_info
+
+ def _get_disk_usage(self) -> Dict[str, float]:
+ disk_usage = {}
+
+ try:
+ for partition in psutil.disk_partitions():
+ try:
+ usage = psutil.disk_usage(partition.mountpoint)
+ disk_usage[partition.mountpoint] = {
+ 'total': usage.total / 1024 / 1024 / 1024,
+ 'used': usage.used / 1024 / 1024 / 1024,
+ 'free': usage.free / 1024 / 1024 / 1024,
+ 'percent': usage.percent
+ }
+ except (OSError, PermissionError):
+ continue
+ except Exception as e:
+ logger.warning(f"Failed to get disk usage: {e}")
+
+ return disk_usage
+
+ def check_system_requirements(self) -> Dict[str, bool]:
+ requirements = {
+ 'python_version': sys.version_info >= (3, 8),
+ 'memory_available': self.get_system_info().available_memory >= 2.0,
+ 'disk_space': self._check_disk_space(),
+ 'permissions': self._check_permissions()
+ }
+
+ return requirements
+
+ def _check_disk_space(self) -> bool:
+ try:
+ check_path = os.path.expanduser("~")
+ usage = psutil.disk_usage(check_path)
+ free_gb = usage.free / 1024 / 1024 / 1024
+ return free_gb >= 1.0
+ except Exception:
+ return True
+
+ def _check_permissions(self) -> bool:
+ try:
+ test_file = os.path.join(os.path.expanduser("~"), ".cursor_free_vip_test")
+ with open(test_file, 'w') as f:
+ f.write("test")
+ os.remove(test_file)
+ return True
+ except Exception:
+ return False
+
+class EnhancedNetworkManager:
+ def __init__(self, timeout: int = 30, max_retries: int = 3):
+ self.timeout = timeout
+ self.max_retries = max_retries
+ self.session = requests.Session()
+ self.session.headers.update({
+ 'User-Agent': 'Cursor-Free-VIP/1.0'
+ })
+
+ def make_request(self, url: str, method: str = 'GET', **kwargs) -> Optional[requests.Response]:
+ for attempt in range(self.max_retries):
+ try:
+ response = self.session.request(
+ method=method,
+ url=url,
+ timeout=self.timeout,
+ **kwargs
+ )
+ response.raise_for_status()
+ return response
+
+ except requests.exceptions.RequestException as e:
+ logger.warning(f"Request attempt {attempt + 1} failed: {e}")
+ if attempt == self.max_retries - 1:
+ logger.error(f"All request attempts failed for {url}")
+ return None
+
+ time.sleep(2 ** attempt)
+
+ return None
+
+ def check_connectivity(self, urls: Optional[List[str]] = None) -> Dict[str, bool]:
+ if urls is None:
+ urls = [
+ 'https://www.google.com',
+ 'https://github.com',
+ 'https://cursor.sh'
+ ]
+
+ results = {}
+
+ with ThreadPoolExecutor(max_workers=5) as executor:
+ future_to_url = {
+ executor.submit(self._check_single_url, url): url
+ for url in urls
+ }
+
+ for future in as_completed(future_to_url):
+ url = future_to_url[future]
+ try:
+ results[url] = future.result()
+ except Exception as e:
+ logger.error(f"Failed to check {url}: {e}")
+ results[url] = False
+
+ return results
+
+ def _check_single_url(self, url: str) -> bool:
+ try:
+ response = self.make_request(url, timeout=10)
+ return response is not None and response.status_code == 200
+ except Exception:
+ return False
+
+path_manager = EnhancedPathManager()
+browser_manager = EnhancedBrowserManager()
+process_manager = EnhancedProcessManager()
+system_manager = EnhancedSystemManager()
+network_manager = EnhancedNetworkManager()
+
+def get_user_documents_path() -> str:
+ return path_manager.get_user_documents_path()
+
+def find_executable(executable_names: List[str]) -> Optional[str]:
+ return path_manager.find_executable(executable_names)
+
+def get_default_browser_path(browser_type: str = 'chrome') -> str:
+ return browser_manager.get_browser_path(browser_type)
+
+def get_default_driver_path(browser_type: str = 'chrome') -> str:
+ return browser_manager.get_driver_path(browser_type)
+
+def parse_time_range(time_str: str) -> Tuple[float, float]:
+ try:
+ if '-' in time_str:
+ parts = time_str.split('-')
+ if len(parts) == 2:
+ return float(parts[0].strip()), float(parts[1].strip())
+ else:
+ value = float(time_str.strip())
+ return value, value
+ except (ValueError, TypeError) as e:
+ logger.warning(f"Failed to parse time range '{time_str}': {e}")
+
+ return 0.5, 1.5
+
+def get_random_wait_time(config: Dict, timing_key: str, default_range: Tuple[float, float] = (0.5, 1.5)) -> float:
+ try:
+ if timing_key in config:
+ time_range = parse_time_range(config[timing_key])
+ return random.uniform(time_range[0], time_range[1])
+ else:
+ return random.uniform(default_range[0], default_range[1])
+ except Exception as e:
+ logger.warning(f"Failed to get random wait time for {timing_key}: {e}")
+ return random.uniform(default_range[0], default_range[1])
+
+def get_linux_cursor_path() -> str:
+ possible_paths = [
+ "/opt/Cursor/resources/app",
+ "/usr/share/cursor/resources/app",
+ "/usr/local/share/cursor/resources/app",
+ os.path.expanduser("~/.local/share/cursor/resources/app"),
+ os.path.expanduser("~/snap/cursor/current/usr/share/cursor/resources/app")
+ ]
+
+ for path in possible_paths:
+ if os.path.exists(path):
+ return path
+
+ cursor_executable = path_manager.find_executable(['cursor'])
+ if cursor_executable:
+ exec_dir = os.path.dirname(cursor_executable)
+ possible_resource_paths = [
+ os.path.join(exec_dir, "resources", "app"),
+ os.path.join(os.path.dirname(exec_dir), "resources", "app"),
+ os.path.join(exec_dir, "..", "resources", "app")
+ ]
+
+ for path in possible_resource_paths:
+ if os.path.exists(path):
+ return os.path.abspath(path)
+
+ return ""
\ No newline at end of file
diff --git a/get_user_token.py b/get_user_token.py
index 6ccbffb..0ab4414 100644
--- a/get_user_token.py
+++ b/get_user_token.py
@@ -1,10 +1,23 @@
import requests
import json
import time
-from colorama import Fore, Style
+import logging
import os
+from typing import Optional, Dict, Any, Union
+from colorama import Fore, Style, init
from config import get_config
+# Initialize colorama
+init(autoreset=True)
+
+# Configure logging
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S"
+)
+logger = logging.getLogger(__name__)
+
# Define emoji constants
EMOJI = {
'START': '๐',
@@ -13,35 +26,85 @@ EMOJI = {
'ERROR': 'โ',
'WAIT': 'โณ',
'INFO': 'โน๏ธ',
- 'WARNING': 'โ ๏ธ'
+ 'WARNING': 'โ ๏ธ',
+ 'TOKEN': '๐',
+ 'REFRESH': '๐'
}
-def refresh_token(token, translator=None):
- """Refresh the token using the Chinese server API
+def _get_message(translator: Any, key: str, fallback: str, **kwargs) -> str:
+ """Get translated message or fallback.
Args:
- token (str): The full WorkosCursorSessionToken cookie value
+ translator: Translator object
+ key: Translation key
+ fallback: Fallback message if translation not available
+ **kwargs: Format parameters for the message
+
+ Returns:
+ str: Translated or fallback message
+ """
+ if translator:
+ return translator.get(key, **kwargs)
+ return fallback.format(**kwargs) if kwargs else fallback
+
+def refresh_token(token: str, translator: Any = None) -> str:
+ """Refresh the token using the refresh server API.
+
+ Args:
+ token: The full WorkosCursorSessionToken cookie value
translator: Optional translator object
Returns:
str: The refreshed access token or original token if refresh fails
"""
try:
+ logger.info("Attempting to refresh token")
+
+ # Validate input
+ if not token or not isinstance(token, str):
+ logger.error("Invalid token provided")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.invalid_token', 'Invalid token provided')}{Style.RESET_ALL}")
+ return token
+
+ # Get configuration
config = get_config(translator)
+
+ # Check if token refresh is enabled
+ if config.has_option('Token', 'enable_refresh') and not config.getboolean('Token', 'enable_refresh'):
+ logger.info("Token refresh is disabled in configuration")
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} {_get_message(translator, 'token.refresh_disabled', 'Token refresh is disabled in configuration')}{Style.RESET_ALL}")
+ return _extract_token_part(token)
+
# Get refresh_server URL from config or use default
refresh_server = config.get('Token', 'refresh_server', fallback='https://token.cursorpro.com.cn')
+ logger.info(f"Using refresh server: {refresh_server}")
# Ensure the token is URL encoded properly
- if '%3A%3A' not in token and '::' in token:
- # Replace :: with URL encoded version if needed
- token = token.replace('::', '%3A%3A')
+ encoded_token = _ensure_token_encoded(token)
# Make the request to the refresh server
- url = f"{refresh_server}/reftoken?token={token}"
+ url = f"{refresh_server}/reftoken?token={encoded_token}"
- print(f"{Fore.CYAN}{EMOJI['INFO']} {translator.get('token.refreshing') if translator else 'Refreshing token...'}{Style.RESET_ALL}")
+ print(f"{Fore.CYAN}{EMOJI['REFRESH']} {_get_message(translator, 'token.refreshing', 'Refreshing token...')}{Style.RESET_ALL}")
- response = requests.get(url, timeout=30)
+ # Set timeout and headers
+ headers = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
+ 'Accept': 'application/json'
+ }
+
+ # Make request with retry logic
+ max_retries = 3
+ for attempt in range(max_retries):
+ try:
+ response = requests.get(url, headers=headers, timeout=30)
+ break
+ except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
+ if attempt < max_retries - 1:
+ logger.warning(f"Request attempt {attempt + 1} failed: {e}. Retrying...")
+ time.sleep(2)
+ else:
+ raise
if response.status_code == 200:
try:
@@ -53,60 +116,140 @@ def refresh_token(token, translator=None):
expire_time = data.get('data', {}).get('expire_time', 'Unknown')
if access_token:
- print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {translator.get('token.refresh_success', days=days_left, expire=expire_time) if translator else f'Token refreshed successfully! Valid for {days_left} days (expires: {expire_time})'}{Style.RESET_ALL}")
+ logger.info(f"Token refreshed successfully. Valid for {days_left} days")
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {_get_message(translator, 'token.refresh_success', 'Token refreshed successfully! Valid for {days} days (expires: {expire})', days=days_left, expire=expire_time)}{Style.RESET_ALL}")
return access_token
else:
- print(f"{Fore.YELLOW}{EMOJI['WARNING']} {translator.get('token.no_access_token') if translator else 'No access token in response'}{Style.RESET_ALL}")
+ logger.warning("No access token in response")
+ print(f"{Fore.YELLOW}{EMOJI['WARNING']} {_get_message(translator, 'token.no_access_token', 'No access token in response')}{Style.RESET_ALL}")
else:
error_msg = data.get('msg', 'Unknown error')
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('token.refresh_failed', error=error_msg) if translator else f'Token refresh failed: {error_msg}'}{Style.RESET_ALL}")
+ logger.error(f"Token refresh failed: {error_msg}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.refresh_failed', 'Token refresh failed: {error}', error=error_msg)}{Style.RESET_ALL}")
except json.JSONDecodeError:
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('token.invalid_response') if translator else 'Invalid JSON response from refresh server'}{Style.RESET_ALL}")
+ logger.error("Invalid JSON response from refresh server")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.invalid_response', 'Invalid JSON response from refresh server')}{Style.RESET_ALL}")
else:
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('token.server_error', status=response.status_code) if translator else f'Refresh server error: HTTP {response.status_code}'}{Style.RESET_ALL}")
+ logger.error(f"Refresh server error: HTTP {response.status_code}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.server_error', 'Refresh server error: HTTP {status}', status=response.status_code)}{Style.RESET_ALL}")
except requests.exceptions.Timeout:
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('token.request_timeout') if translator else 'Request to refresh server timed out'}{Style.RESET_ALL}")
+ logger.error("Request to refresh server timed out")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.request_timeout', 'Request to refresh server timed out')}{Style.RESET_ALL}")
except requests.exceptions.ConnectionError:
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('token.connection_error') if translator else 'Connection error to refresh server'}{Style.RESET_ALL}")
+ logger.error("Connection error to refresh server")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.connection_error', 'Connection error to refresh server')}{Style.RESET_ALL}")
except Exception as e:
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('token.unexpected_error', error=str(e)) if translator else f'Unexpected error during token refresh: {str(e)}'}{Style.RESET_ALL}")
+ logger.error(f"Unexpected error during token refresh: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.unexpected_error', 'Unexpected error during token refresh: {error}', error=str(e))}{Style.RESET_ALL}")
- # Return original token if refresh fails
- return token.split('%3A%3A')[-1] if '%3A%3A' in token else token.split('::')[-1] if '::' in token else token
+ # Return extracted token part if refresh fails
+ return _extract_token_part(token)
-def get_token_from_cookie(cookie_value, translator=None):
- """Extract and process token from cookie value
+def _ensure_token_encoded(token: str) -> str:
+ """Ensure the token is properly URL encoded.
Args:
- cookie_value (str): The WorkosCursorSessionToken cookie value
+ token: The token to encode
+
+ Returns:
+ str: The properly encoded token
+ """
+ if '%3A%3A' not in token and '::' in token:
+ # Replace :: with URL encoded version if needed
+ return token.replace('::', '%3A%3A')
+ return token
+
+def _extract_token_part(token: str) -> str:
+ """Extract the token part from the cookie value.
+
+ Args:
+ token: The full cookie value
+
+ Returns:
+ str: The extracted token part
+ """
+ if '%3A%3A' in token:
+ return token.split('%3A%3A')[-1]
+ elif '::' in token:
+ return token.split('::')[-1]
+ else:
+ return token
+
+def get_token_from_cookie(cookie_value: str, translator: Any = None) -> str:
+ """Extract and process token from cookie value.
+
+ Args:
+ cookie_value: The WorkosCursorSessionToken cookie value
translator: Optional translator object
Returns:
str: The processed token
"""
try:
+ logger.info("Processing token from cookie")
+
+ # Validate input
+ if not cookie_value or not isinstance(cookie_value, str):
+ logger.error("Invalid cookie value provided")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.invalid_cookie', 'Invalid cookie value provided')}{Style.RESET_ALL}")
+ return ""
+
# Try to refresh the token with the API first
+ print(f"{Fore.CYAN}{EMOJI['TOKEN']} {_get_message(translator, 'token.processing', 'Processing token...')}{Style.RESET_ALL}")
refreshed_token = refresh_token(cookie_value, translator)
# If refresh succeeded and returned a different token, use it
- if refreshed_token and refreshed_token != cookie_value:
+ original_token_part = _extract_token_part(cookie_value)
+ if refreshed_token and refreshed_token != original_token_part and refreshed_token != cookie_value:
+ logger.info("Using refreshed token")
return refreshed_token
# If refresh failed or returned same token, use traditional extraction method
- if '%3A%3A' in cookie_value:
- return cookie_value.split('%3A%3A')[-1]
- elif '::' in cookie_value:
- return cookie_value.split('::')[-1]
- else:
- return cookie_value
+ logger.info("Using extracted token part")
+ return original_token_part
except Exception as e:
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('token.extraction_error', error=str(e)) if translator else f'Error extracting token: {str(e)}'}{Style.RESET_ALL}")
+ logger.error(f"Error extracting token: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.extraction_error', 'Error extracting token: {error}', error=str(e))}{Style.RESET_ALL}")
# Fall back to original behavior
- if '%3A%3A' in cookie_value:
- return cookie_value.split('%3A%3A')[-1]
- elif '::' in cookie_value:
- return cookie_value.split('::')[-1]
+ return _extract_token_part(cookie_value)
+
+def validate_token(token: str, translator: Any = None) -> bool:
+ """Validate if the token looks legitimate.
+
+ Args:
+ token: The token to validate
+ translator: Optional translator object
+
+ Returns:
+ bool: True if token looks valid, False otherwise
+ """
+ if not token:
+ logger.warning("Empty token provided")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {_get_message(translator, 'token.empty_token', 'Empty token provided')}{Style.RESET_ALL}")
+ return False
+
+ # Basic validation - JWT tokens typically start with "eyJ"
+ if token.startswith('eyJ') and len(token) > 100 and '.' in token:
+ logger.info("Token appears to be in valid JWT format")
+ return True
+
+ logger.warning("Token does not appear to be in valid JWT format")
+ print(f"{Fore.YELLOW}{EMOJI['WARNING']} {_get_message(translator, 'token.invalid_format', 'Token does not appear to be in valid JWT format')}{Style.RESET_ALL}")
+ return False
+
+if __name__ == "__main__":
+ # Test functionality if run directly
+ try:
+ test_token = input(f"{Fore.CYAN}{EMOJI['TOKEN']} Enter a token to test: {Style.RESET_ALL}")
+ processed_token = get_token_from_cookie(test_token)
+ print(f"\n{Fore.GREEN}{EMOJI['INFO']} Processed token: {processed_token}{Style.RESET_ALL}")
+
+ if validate_token(processed_token):
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} Token appears to be valid{Style.RESET_ALL}")
else:
- return cookie_value
\ No newline at end of file
+ print(f"{Fore.YELLOW}{EMOJI['WARNING']} Token may not be valid{Style.RESET_ALL}")
+ except Exception as e:
+ logger.error(f"Error in test: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} Test error: {str(e)}{Style.RESET_ALL}")
\ No newline at end of file
diff --git a/logo.py b/logo.py
index 1c9123c..594ffa8 100644
--- a/logo.py
+++ b/logo.py
@@ -1,101 +1,103 @@
+import sys
+import platform
+import logging
from colorama import Fore, Style, init
-from dotenv import load_dotenv
-import os
-import shutil
-import re
-
-# Get the current script directory
-current_dir = os.path.dirname(os.path.abspath(__file__))
-# Build the full path to the .env file
-env_path = os.path.join(current_dir, '.env')
-
-# Load environment variables, specifying the .env file path
-load_dotenv(env_path)
-# Get the version number, using the default value if not found
-version = os.getenv('VERSION', '1.0.0')
+from typing import Optional, Dict, List, Any, Tuple, Union
# Initialize colorama
-init()
+init(autoreset=True)
-# get terminal width
-def get_terminal_width():
- try:
- columns, _ = shutil.get_terminal_size()/2
- return columns
- except:
- return 80 # default width
+# Configure logging
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S"
+)
+logger = logging.getLogger(__name__)
-# center display text (not handling Chinese characters)
-def center_multiline_text(text, handle_chinese=False):
- width = get_terminal_width()
- lines = text.split('\n')
- centered_lines = []
-
- for line in lines:
- # calculate actual display width (remove ANSI color codes)
- clean_line = line
- for color in [Fore.CYAN, Fore.YELLOW, Fore.GREEN, Fore.RED, Fore.BLUE, Style.RESET_ALL]:
- clean_line = clean_line.replace(color, '')
-
- # remove all ANSI escape sequences to get the actual length
- ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
- clean_line = ansi_escape.sub('', clean_line)
-
- # calculate display width
- if handle_chinese:
- # consider Chinese characters occupying two positions
- display_width = 0
- for char in clean_line:
- if ord(char) > 127: # non-ASCII characters
- display_width += 2
- else:
- display_width += 1
- else:
- # not handling Chinese characters
- display_width = len(clean_line)
-
- # calculate the number of spaces to add
- padding = max(0, (width - display_width) // 2)
- centered_lines.append(' ' * padding + line)
-
- return '\n'.join(centered_lines)
+# Current version
+version = "1.9.9"
-# original LOGO text
-LOGO_TEXT = f"""{Fore.CYAN}
- โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโ โโโโโโโ โโโโโโโ โโโโโโโ โโโโโโโ โโโโโโโ
- โโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโ
- โโโ โโโ โโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโ
- โโโ โโโ โโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโ โโโโโโโ โโโโโโโโโโโ โโโ
- โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โโโ โโโ โโโ โโโโโโโโโโโโ
- โโโโโโโ โโโโโโโ โโโ โโโโโโโโโโโ โโโโโโโ โโโ โโโ โโโ โโโ โโโ โโโโโโโ
+# ASCII art logo
+LOGO = f"""
+{Fore.CYAN}
+ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโ โโโโโโโ โโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ โโโ โโโโโโโโโโโโโ
+โโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโโ โโโโโโโโโโโโโโ
+โโโ โโโ โโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโ โโโโโโ โโโโโโโโโโโโโโ โโโโโโ โโโ โโโโโโโโโโโโโโ
+โโโ โโโ โโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโ โโโโโโ โโโโโโโโโโโโโโ โโโโโโ โโโโ โโโโโโโโโโโโโโ
+โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โโโ โโโ โโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโ โโโโโโ
+ โโโโโโโ โโโโโโโ โโโ โโโโโโโโโโโ โโโโโโโ โโโ โโโ โโโ โโโ โโโโโโโโโโโโโโโโโโโ โโโโโ โโโโโโ
{Style.RESET_ALL}"""
-DESCRIPTION_TEXT = f"""{Fore.YELLOW}
-Pro Version Activator v{version}{Fore.GREEN}
-Author: Pin Studios (yeongpin)"""
+# Simplified logo for terminals with limited width
+SIMPLIFIED_LOGO = f"""
+{Fore.CYAN}
+ โโโโโโโโโโ โโโโโโโโโโ โโโโโโโโ โโโโโโโ โโโโโโโ
+โโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โโโ โโโ โโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโ
+โโโ โโโ โโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโ
+โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ โโโ
+ โโโโโโโ โโโโโโโ โโโ โโโโโโโโโโโ โโโโโโโ โโโ โโโ
+{Fore.GREEN}FREE VIP {version}{Style.RESET_ALL}
+{Style.RESET_ALL}"""
-CONTRIBUTORS_TEXT = f"""{Fore.BLUE}
-Contributors:
-BasaiCorp aliensb handwerk2016 Nigel1992
-UntaDotMy RenjiYuusei imbajin ahmed98Osama
-bingoohuang mALIk-sHAHId MFaiqKhan httpmerak
-muhammedfurkan plamkatawe Lucaszmv
+# Contributors info
+CURSOR_CONTRIBUTORS = f"""
+{Fore.CYAN}โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
+โ {Fore.YELLOW}CURSOR FREE VIP{Fore.CYAN} โ
+โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
+โ {Fore.GREEN}Author:{Fore.WHITE} yeongpin {Fore.CYAN}โ
+โ {Fore.GREEN}GitHub:{Fore.WHITE} https://github.com/yeongpin/cursor-free-vip {Fore.CYAN}โ
+โ {Fore.GREEN}Version:{Fore.WHITE} {version} {Fore.CYAN}โ
+โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ{Style.RESET_ALL}
"""
-OTHER_INFO_TEXT = f"""{Fore.YELLOW}
-Github: https://github.com/yeongpin/cursor-free-vip{Fore.RED}
-Press 4 to change language | ๆไธ 4 ้ฎๅๆข่ฏญ่จ{Style.RESET_ALL}"""
-# center display LOGO and DESCRIPTION
-CURSOR_LOGO = center_multiline_text(LOGO_TEXT, handle_chinese=False)
-CURSOR_DESCRIPTION = center_multiline_text(DESCRIPTION_TEXT, handle_chinese=False)
-CURSOR_CONTRIBUTORS = center_multiline_text(CONTRIBUTORS_TEXT, handle_chinese=False)
-CURSOR_OTHER_INFO = center_multiline_text(OTHER_INFO_TEXT, handle_chinese=True)
+def get_terminal_width() -> int:
+ """Get terminal width with fallback for different platforms.
+
+ Returns:
+ int: Terminal width in characters
+ """
+ try:
+ # Try to get terminal size using different methods based on platform
+ if platform.system() == "Windows":
+ from shutil import get_terminal_size
+ columns = get_terminal_size().columns
+ else:
+ import os
+ columns = os.get_terminal_size().columns
+
+ return columns
+ except Exception as e:
+ logger.warning(f"Failed to get terminal width: {e}")
+ # Default width if detection fails
+ return 80
-def print_logo():
- print(CURSOR_LOGO)
- print(CURSOR_DESCRIPTION)
- # print(CURSOR_CONTRIBUTORS)
- print(CURSOR_OTHER_INFO)
+def print_logo() -> None:
+ """Print logo with version information based on terminal width."""
+ try:
+ # Get terminal width
+ terminal_width = get_terminal_width()
+
+ # Choose logo based on terminal width
+ if terminal_width < 100:
+ logo = SIMPLIFIED_LOGO
+ else:
+ logo = LOGO
+
+ # Print logo
+ print(logo)
+
+ # Print version info
+ print(f"{Fore.GREEN}Version: {version}{Style.RESET_ALL}")
+ print(f"{Fore.CYAN}{'โ' * min(80, terminal_width)}{Style.RESET_ALL}")
+
+ except Exception as e:
+ logger.error(f"Error printing logo: {e}")
+ # Fallback to simplified version if any error occurs
+ print(SIMPLIFIED_LOGO)
+ print(f"{Fore.GREEN}Version: {version}{Style.RESET_ALL}")
if __name__ == "__main__":
print_logo()
+ print(CURSOR_CONTRIBUTORS)
diff --git a/main.py b/main.py
index 7c608d7..98620b7 100644
--- a/main.py
+++ b/main.py
@@ -2,34 +2,49 @@
# This script allows the user to choose which script to run.
import os
import sys
-import json
-from logo import print_logo, version
-from colorama import Fore, Style, init
import locale
import platform
import requests
import subprocess
+import logging
+from colorama import Fore, Style, init
+from typing import Optional, Dict, List, Any, Tuple, Union
+from pathlib import Path
+
+# Initialize colorama
+init(autoreset=True)
+
+# Configure logging
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S"
+)
+logger = logging.getLogger(__name__)
+
+# Import local modules
+from logo import print_logo, version
from config import get_config, force_update_config
-import shutil
-import re
-from utils import get_user_documents_path
+from utils import get_user_documents_path
# Add these imports for Arabic support
try:
import arabic_reshaper
from bidi.algorithm import get_display
except ImportError:
+ logger.warning("Arabic support modules not found")
arabic_reshaper = None
get_display = None
# Only import windll on Windows systems
if platform.system() == 'Windows':
- import ctypes
- # Only import windll on Windows systems
- from ctypes import windll
-
-# Initialize colorama
-init()
+ try:
+ import ctypes
+ from ctypes import windll
+ except ImportError:
+ logger.warning("Windows-specific modules not found")
+ ctypes = None
+ windll = None
# Define emoji and color constants
EMOJI = {
@@ -53,36 +68,38 @@ EMOJI = {
}
# Function to check if running as frozen executable
-def is_frozen():
+def is_frozen() -> bool:
"""Check if the script is running as a frozen executable."""
return getattr(sys, 'frozen', False)
# Function to check admin privileges (Windows only)
-def is_admin():
+def is_admin() -> bool:
"""Check if the script is running with admin privileges (Windows only)."""
- if platform.system() == 'Windows':
+ if platform.system() == 'Windows' and ctypes and windll:
try:
return ctypes.windll.shell32.IsUserAnAdmin() != 0
- except Exception:
+ except Exception as e:
+ logger.error(f"Error checking admin privileges: {e}")
return False
# Always return True for non-Windows to avoid changing behavior
return True
# Function to restart with admin privileges
-def run_as_admin():
+def run_as_admin() -> bool:
"""Restart the current script with admin privileges (Windows only)."""
- if platform.system() != 'Windows':
+ if platform.system() != 'Windows' or not ctypes or not windll:
return False
try:
args = [sys.executable] + sys.argv
# Request elevation via ShellExecute
- print(f"{Fore.YELLOW}{EMOJI['ADMIN']} Requesting administrator privileges...{Style.RESET_ALL}")
+ print(f"{Fore.YELLOW}{EMOJI['ADMIN']} {translator.get('admin.requesting_privileges') if translator else 'Requesting administrator privileges...'}{Style.RESET_ALL}")
ctypes.windll.shell32.ShellExecuteW(None, "runas", args[0], " ".join('"' + arg + '"' for arg in args[1:]), None, 1)
return True
except Exception as e:
- print(f"{Fore.RED}{EMOJI['ERROR']} Failed to restart with admin privileges: {e}{Style.RESET_ALL}")
+ logger.error(f"Failed to restart with admin privileges: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('admin.restart_failed', error=str(e)) if translator else f'Failed to restart with admin privileges: {e}'}{Style.RESET_ALL}")
return False
class Translator:
@@ -687,119 +704,187 @@ def check_latest_version():
print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('updater.continue_anyway')}{Style.RESET_ALL}")
return
-def main():
- # Check for admin privileges if running as executable on Windows only
- if platform.system() == 'Windows' and is_frozen() and not is_admin():
- print(f"{Fore.YELLOW}{EMOJI['ADMIN']} {translator.get('menu.admin_required')}{Style.RESET_ALL}")
- if run_as_admin():
- sys.exit(0) # Exit after requesting admin privileges
- else:
- print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.admin_required_continue')}{Style.RESET_ALL}")
-
- print_logo()
-
- # Initialize configuration
- config = get_config(translator)
- if not config:
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.config_init_failed')}{Style.RESET_ALL}")
- return
- force_update_config(translator)
-
- if config.getboolean('Utils', 'enabled_update_check'):
- check_latest_version() # Add version check before showing menu
- print_menu()
-
- while True:
- try:
- choice_num = 17
- choice = input(f"\n{EMOJI['ARROW']} {Fore.CYAN}{translator.get('menu.input_choice', choices=f'0-{choice_num}')}: {Style.RESET_ALL}")
-
- match choice:
- case "0":
- print(f"\n{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.exit')}...{Style.RESET_ALL}")
- print(f"{Fore.CYAN}{'โ' * 50}{Style.RESET_ALL}")
- return
- case "1":
- import reset_machine_manual
- reset_machine_manual.run(translator)
- print_menu()
- case "2":
- import cursor_register_manual
- cursor_register_manual.main(translator)
- print_menu()
- case "3":
- import quit_cursor
- quit_cursor.quit_cursor(translator)
- print_menu()
- case "4":
- if select_language():
- print_menu()
- continue
- case "5":
- from oauth_auth import main as oauth_main
- oauth_main('google',translator)
- print_menu()
- case "6":
- from oauth_auth import main as oauth_main
- oauth_main('github',translator)
- print_menu()
- case "7":
- import disable_auto_update
- disable_auto_update.run(translator)
- print_menu()
- case "8":
- import totally_reset_cursor
- totally_reset_cursor.run(translator)
- # print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.fixed_soon')}{Style.RESET_ALL}")
- print_menu()
- case "9":
- import logo
- print(logo.CURSOR_CONTRIBUTORS)
- print_menu()
- case "10":
- from config import print_config
- print_config(get_config(), translator)
- print_menu()
- case "11":
- import bypass_version
- bypass_version.main(translator)
- print_menu()
- case "12":
- import check_user_authorized
- check_user_authorized.main(translator)
- print_menu()
- case "13":
- import bypass_token_limit
- bypass_token_limit.run(translator)
- print_menu()
- case "14":
- import restore_machine_id
- restore_machine_id.run(translator)
- print_menu()
- case "15":
- import delete_cursor_google
- delete_cursor_google.main(translator)
- print_menu()
- case "16":
- from oauth_auth import OAuthHandler
- oauth = OAuthHandler(translator)
- oauth._select_profile()
- print_menu()
- case "17":
- import manual_custom_auth
- manual_custom_auth.main(translator)
- print_menu()
- case _:
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.invalid_choice')}{Style.RESET_ALL}")
- print_menu()
-
- except KeyboardInterrupt:
- print(f"\n{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.program_terminated')}{Style.RESET_ALL}")
- print(f"{Fore.CYAN}{'โ' * 50}{Style.RESET_ALL}")
+def main() -> None:
+ """Main entry point for the application."""
+ try:
+ # Check for admin privileges if running as executable on Windows only
+ if platform.system() == 'Windows' and is_frozen() and not is_admin():
+ logger.warning("Running without admin privileges on Windows")
+ print(f"{Fore.YELLOW}{EMOJI['ADMIN']} {translator.get('menu.admin_required') if translator else 'Administrator privileges are required for some features'}{Style.RESET_ALL}")
+ if run_as_admin():
+ sys.exit(0) # Exit after requesting admin privileges
+ else:
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.admin_required_continue') if translator else 'Continuing without administrator privileges. Some features may not work correctly.'}{Style.RESET_ALL}")
+
+ # Display logo
+ print_logo()
+
+ # Initialize configuration
+ logger.info("Initializing configuration")
+ config = get_config(translator)
+ if not config:
+ logger.error("Failed to initialize configuration")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.config_init_failed') if translator else 'Failed to initialize configuration'}{Style.RESET_ALL}")
return
- except Exception as e:
- print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.error_occurred', error=str(e))}{Style.RESET_ALL}")
- print_menu()
+
+ # Update configuration
+ force_update_config(translator)
+
+ # Check for updates
+ if config.has_option('Utils', 'enabled_update_check') and config.getboolean('Utils', 'enabled_update_check'):
+ logger.info("Checking for updates")
+ check_latest_version()
+
+ # Display menu
+ print_menu()
+
+ # Main menu loop
+ while True:
+ try:
+ choice_num = 17
+ choice = input(f"\n{EMOJI['ARROW']} {Fore.CYAN}{translator.get('menu.input_choice', choices=f'0-{choice_num}') if translator else f'Enter your choice (0-{choice_num})'}: {Style.RESET_ALL}")
+
+ # Process menu choice
+ process_menu_choice(choice)
+
+ except KeyboardInterrupt:
+ logger.info("Program terminated by user (KeyboardInterrupt)")
+ print(f"\n{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.program_terminated') if translator else 'Program terminated by user'}{Style.RESET_ALL}")
+ print(f"{Fore.CYAN}{'โ' * 50}{Style.RESET_ALL}")
+ return
+ except Exception as e:
+ logger.error(f"Unexpected error in main loop: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.unexpected_error', error=str(e)) if translator else f'An unexpected error occurred: {e}'}{Style.RESET_ALL}")
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.please_try_again') if translator else 'Please try again'}{Style.RESET_ALL}")
+
+ except Exception as e:
+ logger.critical(f"Critical error in main function: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.critical_error', error=str(e)) if translator else f'A critical error occurred: {e}'}{Style.RESET_ALL}")
+ print(f"{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.please_restart') if translator else 'Please restart the application'}{Style.RESET_ALL}")
+ return
+
+def process_menu_choice(choice: str) -> None:
+ """Process menu choice and execute corresponding action.
+
+ Args:
+ choice: User's menu choice
+ """
+ try:
+ match choice:
+ case "0":
+ logger.info("User selected to exit")
+ print(f"\n{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.exit') if translator else 'Exiting'}...{Style.RESET_ALL}")
+ print(f"{Fore.CYAN}{'โ' * 50}{Style.RESET_ALL}")
+ sys.exit(0)
+ case "1":
+ logger.info("User selected reset machine manual")
+ import reset_machine_manual
+ reset_machine_manual.run(translator)
+ print_menu()
+ case "2":
+ logger.info("User selected cursor register manual")
+ import cursor_register_manual
+ cursor_register_manual.main(translator)
+ print_menu()
+ case "3":
+ logger.info("User selected quit cursor")
+ import quit_cursor
+ quit_cursor.quit_cursor(translator)
+ print_menu()
+ case "4":
+ logger.info("User selected language settings")
+ if select_language():
+ print_menu()
+ return
+ case "5":
+ logger.info("User selected Google OAuth")
+ from oauth_auth import main as oauth_main
+ oauth_main('google', translator)
+ print_menu()
+ case "6":
+ logger.info("User selected GitHub OAuth")
+ from oauth_auth import main as oauth_main
+ oauth_main('github', translator)
+ print_menu()
+ case "7":
+ logger.info("User selected disable auto update")
+ import disable_auto_update
+ disable_auto_update.run(translator)
+ print_menu()
+ case "8":
+ logger.info("User selected totally reset cursor")
+ import totally_reset_cursor
+ totally_reset_cursor.run(translator)
+ print_menu()
+ case "9":
+ logger.info("User selected view contributors")
+ import logo
+ print(logo.CURSOR_CONTRIBUTORS)
+ print_menu()
+ case "10":
+ logger.info("User selected view config")
+ from config import print_config
+ print_config(get_config(), translator)
+ print_menu()
+ case "11":
+ logger.info("User selected bypass version")
+ import bypass_version
+ bypass_version.main(translator)
+ print_menu()
+ case "12":
+ logger.info("User selected check user authorized")
+ import check_user_authorized
+ check_user_authorized.main(translator)
+ print_menu()
+ case "13":
+ logger.info("User selected bypass token limit")
+ import bypass_token_limit
+ bypass_token_limit.run(translator)
+ print_menu()
+ case "14":
+ logger.info("User selected restore machine ID")
+ import restore_machine_id
+ restore_machine_id.run(translator)
+ print_menu()
+ case "15":
+ logger.info("User selected delete cursor Google")
+ import delete_cursor_google
+ delete_cursor_google.main(translator)
+ print_menu()
+ case "16":
+ logger.info("User selected select profile")
+ from oauth_auth import OAuthHandler
+ oauth = OAuthHandler(translator)
+ oauth._select_profile()
+ print_menu()
+ case "17":
+ logger.info("User selected manual custom auth")
+ import manual_custom_auth
+ manual_custom_auth.main(translator)
+ print_menu()
+ case _:
+ logger.warning(f"Invalid choice: {choice}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.invalid_choice') if translator else 'Invalid choice'}{Style.RESET_ALL}")
+ print_menu()
+ except ImportError as e:
+ logger.error(f"Failed to import module: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.module_not_found', error=str(e)) if translator else f'Module not found: {e}'}{Style.RESET_ALL}")
+ print_menu()
+ except Exception as e:
+ logger.error(f"Error processing menu choice: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.action_failed', error=str(e)) if translator else f'Action failed: {e}'}{Style.RESET_ALL}")
+ print_menu()
if __name__ == "__main__":
- main()
\ No newline at end of file
+ # Initialize translator
+ translator = Translator()
+
+ try:
+ main()
+ except KeyboardInterrupt:
+ print(f"\n{Fore.YELLOW}{EMOJI['INFO']} {translator.get('menu.program_terminated') if translator else 'Program terminated by user'}{Style.RESET_ALL}")
+ sys.exit(0)
+ except Exception as e:
+ logger.critical(f"Unhandled exception: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {translator.get('menu.unhandled_exception', error=str(e)) if translator else f'Unhandled exception: {e}'}{Style.RESET_ALL}")
+ sys.exit(1)
\ No newline at end of file
diff --git a/quit_cursor.py b/quit_cursor.py
index 117d86c..8c772b4 100644
--- a/quit_cursor.py
+++ b/quit_cursor.py
@@ -1,11 +1,22 @@
import psutil
import time
-from colorama import Fore, Style, init
+import logging
+import platform
import sys
import os
+from colorama import Fore, Style, init
+from typing import Optional, Dict, List, Any, Tuple, Union, Set
# Initialize colorama
-init()
+init(autoreset=True)
+
+# Configure logging
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S"
+)
+logger = logging.getLogger(__name__)
# Define emoji constants
EMOJI = {
@@ -13,43 +24,126 @@ EMOJI = {
"SUCCESS": "โ
",
"ERROR": "โ",
"INFO": "โน๏ธ",
- "WAIT": "โณ"
+ "WAIT": "โณ",
+ "KILL": "๐",
+ "SEARCH": "๐"
}
class CursorQuitter:
- def __init__(self, timeout=5, translator=None):
- self.timeout = timeout
- self.translator = translator # Use the passed translator
+ """Class to handle termination of Cursor processes."""
+
+ def __init__(self, timeout: int = 5, translator: Any = None):
+ """Initialize CursorQuitter.
- def quit_cursor(self):
- """Gently close Cursor processes"""
- try:
- print(f"{Fore.CYAN}{EMOJI['PROCESS']} {self.translator.get('quit_cursor.start')}...{Style.RESET_ALL}")
- cursor_processes = []
+ Args:
+ timeout: Maximum time to wait for processes to terminate naturally
+ translator: Optional translator for internationalization
+ """
+ self.timeout = max(1, timeout) # Ensure timeout is at least 1 second
+ self.translator = translator
+
+ def _get_message(self, key: str, fallback: str, **kwargs) -> str:
+ """Get translated message or fallback.
+
+ Args:
+ key: Translation key
+ fallback: Fallback message if translation not available
+ **kwargs: Format parameters for the message
- # Collect all Cursor processes
- for proc in psutil.process_iter(['pid', 'name']):
- try:
- if proc.info['name'].lower() in ['cursor.exe', 'cursor']:
- cursor_processes.append(proc)
- except (psutil.NoSuchProcess, psutil.AccessDenied):
+ Returns:
+ str: Translated or fallback message
+ """
+ if self.translator:
+ return self.translator.get(key, **kwargs)
+ return fallback.format(**kwargs) if kwargs else fallback
+
+ def _find_cursor_processes(self) -> List[psutil.Process]:
+ """Find all Cursor processes.
+
+ Returns:
+ List[psutil.Process]: List of Cursor processes
+ """
+ cursor_processes = []
+ cursor_names = {
+ 'windows': ['cursor.exe', 'cursor helper.exe', 'cursor crash handler.exe'],
+ 'darwin': ['Cursor', 'Cursor Helper', 'Cursor Crash Handler'],
+ 'linux': ['cursor', 'cursor-helper', 'cursor-crash-handler']
+ }
+
+ # Get platform-specific process names
+ system = platform.system().lower()
+ if system in cursor_names:
+ target_names = cursor_names[system]
+ else:
+ # Fallback to all possible names
+ target_names = [name for names in cursor_names.values() for name in names]
+
+ logger.info(f"Looking for Cursor processes with names: {target_names}")
+ print(f"{Fore.CYAN}{EMOJI['SEARCH']} {self._get_message('quit_cursor.searching', 'Searching for Cursor processes...')}{Style.RESET_ALL}")
+
+ # Collect all Cursor processes
+ for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
+ try:
+ proc_name = proc.info['name'].lower() if proc.info['name'] else ""
+
+ # Check process name
+ if any(target.lower() in proc_name for target in target_names):
+ cursor_processes.append(proc)
continue
-
+
+ # Check command line for additional detection
+ if proc.info['cmdline']:
+ cmdline = " ".join(proc.info['cmdline']).lower()
+ if 'cursor' in cmdline and ('electron' in cmdline or 'app' in cmdline):
+ cursor_processes.append(proc)
+
+ except (psutil.NoSuchProcess, psutil.AccessDenied, Exception) as e:
+ logger.warning(f"Error accessing process: {e}")
+ continue
+
+ return cursor_processes
+
+ def quit_cursor(self) -> bool:
+ """Gently close Cursor processes.
+
+ Returns:
+ bool: True if all processes were terminated successfully, False otherwise
+ """
+ try:
+ msg = self._get_message('quit_cursor.start', 'Attempting to close Cursor processes')
+ logger.info(msg)
+ print(f"{Fore.CYAN}{EMOJI['PROCESS']} {msg}...{Style.RESET_ALL}")
+
+ # Find Cursor processes
+ cursor_processes = self._find_cursor_processes()
+
if not cursor_processes:
- print(f"{Fore.GREEN}{EMOJI['INFO']} {self.translator.get('quit_cursor.no_process')}{Style.RESET_ALL}")
+ msg = self._get_message('quit_cursor.no_process', 'No Cursor processes found')
+ logger.info(msg)
+ print(f"{Fore.GREEN}{EMOJI['INFO']} {msg}{Style.RESET_ALL}")
return True
+ # Log found processes
+ logger.info(f"Found {len(cursor_processes)} Cursor processes")
+ print(f"{Fore.CYAN}{EMOJI['INFO']} {self._get_message('quit_cursor.processes_found', 'Found {count} Cursor processes', count=len(cursor_processes))}{Style.RESET_ALL}")
+
# Gently request processes to terminate
for proc in cursor_processes:
try:
if proc.is_running():
- print(f"{Fore.YELLOW}{EMOJI['PROCESS']} {self.translator.get('quit_cursor.terminating', pid=proc.pid)}...{Style.RESET_ALL}")
+ msg = self._get_message('quit_cursor.terminating', 'Terminating process {pid}', pid=proc.pid)
+ logger.info(f"Terminating process {proc.pid}")
+ print(f"{Fore.YELLOW}{EMOJI['PROCESS']} {msg}...{Style.RESET_ALL}")
proc.terminate()
- except (psutil.NoSuchProcess, psutil.AccessDenied):
+ except (psutil.NoSuchProcess, psutil.AccessDenied, Exception) as e:
+ logger.warning(f"Error terminating process {proc.pid}: {e}")
continue
# Wait for processes to terminate naturally
- print(f"{Fore.CYAN}{EMOJI['WAIT']} {self.translator.get('quit_cursor.waiting')}...{Style.RESET_ALL}")
+ msg = self._get_message('quit_cursor.waiting', f'Waiting up to {self.timeout} seconds for processes to close')
+ logger.info(f"Waiting up to {self.timeout} seconds for processes to close")
+ print(f"{Fore.CYAN}{EMOJI['WAIT']} {msg}...{Style.RESET_ALL}")
+
start_time = time.time()
while time.time() - start_time < self.timeout:
still_running = []
@@ -57,33 +151,89 @@ class CursorQuitter:
try:
if proc.is_running():
still_running.append(proc)
- except (psutil.NoSuchProcess, psutil.AccessDenied):
+ except (psutil.NoSuchProcess, psutil.AccessDenied, Exception):
continue
if not still_running:
- print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {self.translator.get('quit_cursor.success')}{Style.RESET_ALL}")
+ msg = self._get_message('quit_cursor.success', 'All Cursor processes have been closed successfully')
+ logger.info("All Cursor processes have been closed successfully")
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {msg}{Style.RESET_ALL}")
return True
time.sleep(0.5)
- # If processes are still running after timeout
+ # If processes are still running after timeout, try to kill them
if still_running:
process_list = ", ".join([str(p.pid) for p in still_running])
- print(f"{Fore.RED}{EMOJI['ERROR']} {self.translator.get('quit_cursor.timeout', pids=process_list)}{Style.RESET_ALL}")
- return False
+ msg = self._get_message('quit_cursor.timeout', 'Timeout reached. Some processes are still running: {pids}', pids=process_list)
+ logger.warning(f"Timeout reached. Still running: {process_list}")
+ print(f"{Fore.YELLOW}{EMOJI['WAIT']} {msg}{Style.RESET_ALL}")
+
+ # Try to kill remaining processes
+ print(f"{Fore.RED}{EMOJI['KILL']} {self._get_message('quit_cursor.force_kill', 'Attempting to force kill remaining processes')}{Style.RESET_ALL}")
+ for proc in still_running:
+ try:
+ if proc.is_running():
+ logger.info(f"Force killing process {proc.pid}")
+ proc.kill()
+ except (psutil.NoSuchProcess, psutil.AccessDenied, Exception) as e:
+ logger.error(f"Error killing process {proc.pid}: {e}")
+ continue
+
+ # Check if all processes are now killed
+ time.sleep(1)
+ final_check = [p for p in still_running if p.is_running()]
+ if not final_check:
+ msg = self._get_message('quit_cursor.force_success', 'All Cursor processes have been forcefully terminated')
+ logger.info("All Cursor processes have been forcefully terminated")
+ print(f"{Fore.GREEN}{EMOJI['SUCCESS']} {msg}{Style.RESET_ALL}")
+ return True
+ else:
+ failed_list = ", ".join([str(p.pid) for p in final_check])
+ msg = self._get_message('quit_cursor.force_failed', 'Failed to terminate some processes: {pids}', pids=failed_list)
+ logger.error(f"Failed to terminate processes: {failed_list}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} {msg}{Style.RESET_ALL}")
+ return False
return True
except Exception as e:
- print(f"{Fore.RED}{EMOJI['ERROR']} {self.translator.get('quit_cursor.error', error=str(e))}{Style.RESET_ALL}")
+ logger.error(f"Error in quit_cursor: {e}")
+ msg = self._get_message('quit_cursor.error', 'An error occurred: {error}', error=str(e))
+ print(f"{Fore.RED}{EMOJI['ERROR']} {msg}{Style.RESET_ALL}")
return False
-def quit_cursor(translator=None, timeout=5):
- """Convenient function for directly calling the quit function"""
- quitter = CursorQuitter(timeout, translator)
- return quitter.quit_cursor()
+def quit_cursor(translator: Any = None, timeout: int = 5) -> bool:
+ """Convenient function for directly calling the quit function.
+
+ Args:
+ translator: Optional translator for internationalization
+ timeout: Maximum time to wait for processes to terminate naturally
+
+ Returns:
+ bool: True if all processes were terminated successfully, False otherwise
+ """
+ try:
+ quitter = CursorQuitter(timeout, translator)
+ return quitter.quit_cursor()
+ except Exception as e:
+ logger.error(f"Error in quit_cursor function: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} An unexpected error occurred: {str(e)}{Style.RESET_ALL}")
+ return False
if __name__ == "__main__":
- # If run directly, use the default translator
- from main import translator as main_translator
- quit_cursor(main_translator)
\ No newline at end of file
+ try:
+ # If run directly, try to use the default translator
+ try:
+ from main import translator as main_translator
+ result = quit_cursor(main_translator)
+ except ImportError:
+ logger.warning("Failed to import translator from main.py, running without translation")
+ result = quit_cursor()
+
+ # Exit with appropriate status code
+ sys.exit(0 if result else 1)
+ except Exception as e:
+ logger.critical(f"Critical error: {e}")
+ print(f"{Fore.RED}{EMOJI['ERROR']} Critical error: {str(e)}{Style.RESET_ALL}")
+ sys.exit(1)
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index d4698f9..cfbe475 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,13 +1,29 @@
-watchdog
-python-dotenv>=1.0.0
-colorama>=0.4.6
-requests
-psutil>=5.8.0
-pywin32; platform_system == "Windows"
-pyinstaller
-DrissionPage>=4.0.0
-selenium
-webdriver_manager
-arabic-reshaper
-python-bidi
-faker
\ No newline at end of file
+# Core dependencies
+colorama>=0.4.6,<0.5.0
+requests>=2.31.0,<3.0.0
+python-dotenv>=1.0.0,<2.0.0
+psutil>=5.9.5,<6.0.0
+watchdog>=3.0.0,<4.0.0
+
+# Web automation
+selenium>=4.14.0,<5.0.0
+webdriver_manager>=4.0.0,<5.0.0
+DrissionPage>=4.0.0,<5.0.0
+
+# Internationalization
+arabic-reshaper>=3.0.0,<4.0.0
+python-bidi>=0.4.2,<0.5.0
+
+# Data generation
+faker>=19.3.0,<20.0.0
+
+# Packaging
+pyinstaller>=6.0.0,<7.0.0
+
+# Windows-specific dependencies
+pywin32>=306; platform_system == "Windows"
+
+# Optional dependencies
+tqdm>=4.66.1,<5.0.0 # Progress bars
+cryptography>=41.0.4,<42.0.0 # Secure encryption
+pillow>=10.0.0,<11.0.0 # Image processing
\ No newline at end of file
diff --git a/utils.py b/utils.py
index 1d22979..d7f1554 100644
--- a/utils.py
+++ b/utils.py
@@ -2,233 +2,332 @@ import os
import sys
import platform
import random
+import shutil
+import logging
+from typing import Optional, Dict, List, Union, Tuple
-def get_user_documents_path():
- """Get user documents path"""
+# Configure logging
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s - %(levelname)s - %(message)s",
+ datefmt="%Y-%m-%d %H:%M:%S"
+)
+logger = logging.getLogger(__name__)
+
+def get_user_documents_path() -> str:
+ """Get user documents path across different operating systems.
+
+ Returns:
+ str: Path to user's Documents directory
+ """
if platform.system() == "Windows":
try:
import winreg
- # ๆๅผๆณจๅ่กจ
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders") as key:
- # ่ทๅ "Personal" ้ฎ็ๅผ๏ผ่ฟๆๅ็จๆท็ๆๆกฃ็ฎๅฝ
documents_path, _ = winreg.QueryValueEx(key, "Personal")
return documents_path
except Exception as e:
- # fallback
+ logger.warning(f"Failed to get Documents path from registry: {e}")
return os.path.expanduser("~\\Documents")
- else:
+ elif platform.system() == "Darwin": # macOS
+ return os.path.expanduser("~/Documents")
+ else: # Linux and other Unix-like systems
+ # Check for XDG user directories
+ try:
+ with open(os.path.expanduser("~/.config/user-dirs.dirs"), "r") as f:
+ for line in f:
+ if line.startswith("XDG_DOCUMENTS_DIR"):
+ path = line.split("=")[1].strip().strip('"').replace("$HOME", os.path.expanduser("~"))
+ if os.path.exists(path):
+ return path
+ except (FileNotFoundError, IOError):
+ pass
+
+ # Fallback to ~/Documents
return os.path.expanduser("~/Documents")
-
-def get_default_driver_path(browser_type='chrome'):
- """Get default driver path based on browser type"""
- browser_type = browser_type.lower()
- if browser_type == 'chrome':
- return get_default_chrome_driver_path()
- elif browser_type == 'edge':
- return get_default_edge_driver_path()
- elif browser_type == 'firefox':
- return get_default_firefox_driver_path()
- elif browser_type == 'brave':
- # Brave ไฝฟ็จ Chrome ็ driver
- return get_default_chrome_driver_path()
- else:
- # Default to Chrome if browser type is unknown
- return get_default_chrome_driver_path()
-def get_default_chrome_driver_path():
- """Get default Chrome driver path"""
+def find_executable(executable_names: List[str]) -> Optional[str]:
+ """Find executable in PATH by trying multiple possible names.
+
+ Args:
+ executable_names: List of possible executable names to try
+
+ Returns:
+ Path to the executable if found, None otherwise
+ """
+ for name in executable_names:
+ try:
+ path = shutil.which(name)
+ if path:
+ return path
+ except Exception:
+ continue
+ return None
+
+def get_default_driver_path(browser_type: str = 'chrome') -> str:
+ """Get default driver path based on browser type.
+
+ Args:
+ browser_type: Type of browser ('chrome', 'edge', 'firefox', 'brave')
+
+ Returns:
+ str: Path to the browser driver
+ """
+ browser_type = browser_type.lower()
+ driver_map = {
+ 'chrome': get_default_chrome_driver_path,
+ 'edge': get_default_edge_driver_path,
+ 'firefox': get_default_firefox_driver_path,
+ 'brave': get_default_chrome_driver_path, # Brave uses Chrome driver
+ 'opera': get_default_chrome_driver_path, # Opera uses Chrome driver
+ 'operagx': get_default_chrome_driver_path # OperaGX uses Chrome driver
+ }
+
+ driver_func = driver_map.get(browser_type, get_default_chrome_driver_path)
+ return driver_func()
+
+def get_default_chrome_driver_path() -> str:
+ """Get default Chrome driver path based on platform."""
if sys.platform == "win32":
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", "chromedriver.exe")
elif sys.platform == "darwin":
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", "chromedriver")
- else:
+ else: # Linux and other Unix-like systems
+ # Try to find chromedriver in PATH first
+ path = find_executable(["chromedriver"])
+ if path:
+ return path
return "/usr/local/bin/chromedriver"
-def get_default_edge_driver_path():
- """Get default Edge driver path"""
+def get_default_edge_driver_path() -> str:
+ """Get default Edge driver path based on platform."""
if sys.platform == "win32":
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", "msedgedriver.exe")
elif sys.platform == "darwin":
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", "msedgedriver")
- else:
+ else: # Linux and other Unix-like systems
+ path = find_executable(["msedgedriver"])
+ if path:
+ return path
return "/usr/local/bin/msedgedriver"
-def get_default_firefox_driver_path():
- """Get default Firefox driver path"""
+def get_default_firefox_driver_path() -> str:
+ """Get default Firefox driver path based on platform."""
if sys.platform == "win32":
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", "geckodriver.exe")
elif sys.platform == "darwin":
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "drivers", "geckodriver")
- else:
+ else: # Linux and other Unix-like systems
+ path = find_executable(["geckodriver"])
+ if path:
+ return path
return "/usr/local/bin/geckodriver"
-def get_default_brave_driver_path():
- """Get default Brave driver path (uses Chrome driver)"""
- # Brave ๆต่งๅจๅบไบ Chromium๏ผๆไปฅไฝฟ็จ็ธๅ็ chromedriver
- return get_default_chrome_driver_path()
-
-def get_default_browser_path(browser_type='chrome'):
- """Get default browser executable path"""
+def get_default_browser_path(browser_type: str = 'chrome') -> str:
+ """Get default browser executable path based on platform and browser type.
+
+ Args:
+ browser_type: Type of browser ('chrome', 'edge', 'firefox', 'brave', 'opera', 'operagx')
+
+ Returns:
+ str: Path to the browser executable
+ """
browser_type = browser_type.lower()
+ # Platform-specific browser paths
if sys.platform == "win32":
- if browser_type == 'chrome':
- # ๅฐ่ฏๅจ PATH ไธญๆพๅฐ Chrome
- try:
- import shutil
- chrome_in_path = shutil.which("chrome")
- if chrome_in_path:
- return chrome_in_path
- except:
- pass
- # ไฝฟ็จ้ป่ฎค่ทฏๅพ
- return r"C:\Program Files\Google\Chrome\Application\chrome.exe"
- elif browser_type == 'edge':
- return r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
- elif browser_type == 'firefox':
- return r"C:\Program Files\Mozilla Firefox\firefox.exe"
- elif browser_type == 'opera':
- # ๅฐ่ฏๅคไธชๅฏ่ฝ็ Opera ่ทฏๅพ
- opera_paths = [
- r"C:\Program Files\Opera\opera.exe",
- r"C:\Program Files (x86)\Opera\opera.exe",
- os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera', 'launcher.exe'),
- os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera', 'opera.exe')
- ]
- for path in opera_paths:
- if os.path.exists(path):
- return path
- return opera_paths[0] # ่ฟๅ็ฌฌไธไธช่ทฏๅพ๏ผๅณไฝฟๅฎไธๅญๅจ
- elif browser_type == 'operagx':
- # ๅฐ่ฏๅคไธชๅฏ่ฝ็ Opera GX ่ทฏๅพ
- operagx_paths = [
- os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera GX', 'launcher.exe'),
- os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera GX', 'opera.exe'),
- r"C:\Program Files\Opera GX\opera.exe",
- r"C:\Program Files (x86)\Opera GX\opera.exe"
- ]
- for path in operagx_paths:
- if os.path.exists(path):
- return path
- return operagx_paths[0] # ่ฟๅ็ฌฌไธไธช่ทฏๅพ๏ผๅณไฝฟๅฎไธๅญๅจ
- elif browser_type == 'brave':
- # Brave ๆต่งๅจ็้ป่ฎคๅฎ่ฃ
่ทฏๅพ
- paths = [
- os.path.join(os.environ.get('PROGRAMFILES', ''), 'BraveSoftware/Brave-Browser/Application/brave.exe'),
- os.path.join(os.environ.get('PROGRAMFILES(X86)', ''), 'BraveSoftware/Brave-Browser/Application/brave.exe'),
- os.path.join(os.environ.get('LOCALAPPDATA', ''), 'BraveSoftware/Brave-Browser/Application/brave.exe')
- ]
- for path in paths:
- if os.path.exists(path):
- return path
- return paths[0] # ่ฟๅ็ฌฌไธไธช่ทฏๅพ๏ผๅณไฝฟๅฎไธๅญๅจ
-
+ return _get_windows_browser_path(browser_type)
elif sys.platform == "darwin":
- if browser_type == 'chrome':
- return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
- elif browser_type == 'edge':
- return "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
- elif browser_type == 'firefox':
- return "/Applications/Firefox.app/Contents/MacOS/firefox"
- elif browser_type == 'brave':
- return "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
- elif browser_type == 'opera':
- return "/Applications/Opera.app/Contents/MacOS/Opera"
- elif browser_type == 'operagx':
- return "/Applications/Opera GX.app/Contents/MacOS/Opera"
-
- else: # Linux
- if browser_type == 'chrome':
- # ๅฐ่ฏๅค็งๅฏ่ฝ็ๅ็งฐ
- chrome_names = ["google-chrome", "chrome", "chromium", "chromium-browser"]
- for name in chrome_names:
- try:
- import shutil
- path = shutil.which(name)
- if path:
- return path
- except:
- pass
- return "/usr/bin/google-chrome"
- elif browser_type == 'edge':
- return "/usr/bin/microsoft-edge"
- elif browser_type == 'firefox':
- return "/usr/bin/firefox"
- elif browser_type == 'opera':
- return "/usr/bin/opera"
- elif browser_type == 'operagx':
- # ๅฐ่ฏๅธธ่ง็ Opera GX ่ทฏๅพ
- operagx_names = ["opera-gx"]
- for name in operagx_names:
- try:
- import shutil
- path = shutil.which(name)
- if path:
- return path
- except:
- pass
- return "/usr/bin/opera-gx"
- elif browser_type == 'brave':
- # ๅฐ่ฏๅธธ่ง็ Brave ่ทฏๅพ
- brave_names = ["brave", "brave-browser"]
- for name in brave_names:
- try:
- import shutil
- path = shutil.which(name)
- if path:
- return path
- except:
- pass
- return "/usr/bin/brave-browser"
-
- # ๅฆๆๆพไธๅฐๆๅฎ็ๆต่งๅจ็ฑปๅ๏ผๅ่ฟๅ Chrome ็่ทฏๅพ
- return get_default_browser_path('chrome')
+ return _get_macos_browser_path(browser_type)
+ else: # Linux and other Unix-like systems
+ return _get_linux_browser_path(browser_type)
-def get_linux_cursor_path():
- """Get Linux Cursor path"""
+def _get_windows_browser_path(browser_type: str) -> str:
+ """Get browser path for Windows."""
+ browser_paths = {
+ 'chrome': [
+ shutil.which("chrome"),
+ r"C:\Program Files\Google\Chrome\Application\chrome.exe",
+ r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
+ os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Google', 'Chrome', 'Application', 'chrome.exe')
+ ],
+ 'edge': [
+ shutil.which("msedge"),
+ r"C:\Program Files\Microsoft\Edge\Application\msedge.exe",
+ r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
+ ],
+ 'firefox': [
+ shutil.which("firefox"),
+ r"C:\Program Files\Mozilla Firefox\firefox.exe",
+ r"C:\Program Files (x86)\Mozilla Firefox\firefox.exe"
+ ],
+ 'opera': [
+ shutil.which("opera"),
+ r"C:\Program Files\Opera\opera.exe",
+ r"C:\Program Files (x86)\Opera\opera.exe",
+ os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera', 'launcher.exe'),
+ os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera', 'opera.exe')
+ ],
+ 'operagx': [
+ shutil.which("opera"),
+ os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera GX', 'launcher.exe'),
+ os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Opera GX', 'opera.exe'),
+ r"C:\Program Files\Opera GX\opera.exe",
+ r"C:\Program Files (x86)\Opera GX\opera.exe"
+ ],
+ 'brave': [
+ shutil.which("brave"),
+ os.path.join(os.environ.get('PROGRAMFILES', ''), 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
+ os.path.join(os.environ.get('PROGRAMFILES(X86)', ''), 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe'),
+ os.path.join(os.environ.get('LOCALAPPDATA', ''), 'BraveSoftware', 'Brave-Browser', 'Application', 'brave.exe')
+ ]
+ }
+
+ # Return first existing path
+ paths = browser_paths.get(browser_type, browser_paths['chrome'])
+ for path in paths:
+ if path and os.path.exists(path):
+ return path
+
+ # Return first path as fallback
+ return next((p for p in paths if p), r"C:\Program Files\Google\Chrome\Application\chrome.exe")
+
+def _get_macos_browser_path(browser_type: str) -> str:
+ """Get browser path for macOS."""
+ browser_paths = {
+ 'chrome': [
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
+ "~/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
+ ],
+ 'edge': [
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
+ "~/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
+ ],
+ 'firefox': [
+ "/Applications/Firefox.app/Contents/MacOS/firefox",
+ "~/Applications/Firefox.app/Contents/MacOS/firefox"
+ ],
+ 'brave': [
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
+ "~/Applications/Brave Browser.app/Contents/MacOS/Brave Browser"
+ ],
+ 'opera': [
+ "/Applications/Opera.app/Contents/MacOS/Opera",
+ "~/Applications/Opera.app/Contents/MacOS/Opera"
+ ],
+ 'operagx': [
+ "/Applications/Opera GX.app/Contents/MacOS/Opera",
+ "~/Applications/Opera GX.app/Contents/MacOS/Opera"
+ ]
+ }
+
+ # Return first existing path
+ paths = browser_paths.get(browser_type, browser_paths['chrome'])
+ for path in paths:
+ expanded_path = os.path.expanduser(path)
+ if os.path.exists(expanded_path):
+ return expanded_path
+
+ # Return first path as fallback
+ return os.path.expanduser(paths[0])
+
+def _get_linux_browser_path(browser_type: str) -> str:
+ """Get browser path for Linux."""
+ browser_executables = {
+ 'chrome': ["google-chrome", "chrome", "chromium", "chromium-browser"],
+ 'edge': ["microsoft-edge", "msedge"],
+ 'firefox': ["firefox", "firefox-esr"],
+ 'opera': ["opera"],
+ 'operagx': ["opera-gx", "opera"],
+ 'brave': ["brave-browser", "brave"]
+ }
+
+ # Try to find executable in PATH
+ executables = browser_executables.get(browser_type, browser_executables['chrome'])
+ path = find_executable(executables)
+ if path:
+ return path
+
+ # Fallback to common locations
+ common_locations = {
+ 'chrome': "/usr/bin/google-chrome",
+ 'edge': "/usr/bin/microsoft-edge",
+ 'firefox': "/usr/bin/firefox",
+ 'opera': "/usr/bin/opera",
+ 'operagx': "/usr/bin/opera",
+ 'brave': "/usr/bin/brave-browser"
+ }
+
+ return common_locations.get(browser_type, common_locations['chrome'])
+
+def get_linux_cursor_path() -> str:
+ """Get Linux Cursor path by checking multiple possible locations."""
possible_paths = [
"/opt/Cursor/resources/app",
"/usr/share/cursor/resources/app",
"/opt/cursor-bin/resources/app",
"/usr/lib/cursor/resources/app",
- os.path.expanduser("~/.local/share/cursor/resources/app")
+ os.path.expanduser("~/.local/share/cursor/resources/app"),
+ # Add extracted AppImage paths
+ *[p for p in [os.path.expanduser("~/squashfs-root/usr/share/cursor/resources/app")] if os.path.exists(p)]
]
- # return the first path that exists
- return next((path for path in possible_paths if os.path.exists(path)), possible_paths[0])
+ # Return first existing path or default if none exists
+ for path in possible_paths:
+ if os.path.exists(path):
+ return path
+
+ # Log warning if no path found
+ logger.warning("No Cursor installation found in common Linux paths")
+ return possible_paths[0]
-def get_random_wait_time(config, timing_key):
- """Get random wait time based on configuration timing settings
+def parse_time_range(time_str: str) -> Tuple[float, float]:
+ """Parse a time range string into min and max values.
Args:
- config (dict): Configuration dictionary containing timing settings
- timing_key (str): Key to look up in the timing settings
+ time_str: String representing time range (e.g., "0.5-1.5" or "0.5,1.5")
+
+ Returns:
+ Tuple of (min_time, max_time)
+ """
+ try:
+ if isinstance(time_str, (int, float)):
+ return float(time_str), float(time_str)
+
+ if '-' in time_str:
+ min_time, max_time = map(float, time_str.split('-'))
+ elif ',' in time_str:
+ min_time, max_time = map(float, time_str.split(','))
+ else:
+ min_time = max_time = float(time_str)
+
+ return min_time, max_time
+ except (ValueError, TypeError):
+ return 0.5, 1.5
+
+def get_random_wait_time(config: Dict, timing_key: str, default_range: Tuple[float, float] = (0.5, 1.5)) -> float:
+ """Get random wait time based on configuration timing settings.
+
+ Args:
+ config: Configuration dictionary containing timing settings
+ timing_key: Key to look up in the timing settings
+ default_range: Default time range to use if config value is invalid
Returns:
float: Random wait time in seconds
"""
try:
# Get timing value from config
+ if not config or 'Timing' not in config:
+ return random.uniform(*default_range)
+
timing = config.get('Timing', {}).get(timing_key)
if not timing:
- # Default to 0.5-1.5 seconds if timing not found
- return random.uniform(0.5, 1.5)
-
- # Check if timing is a range (e.g., "0.5-1.5" or "0.5,1.5")
- if isinstance(timing, str):
- if '-' in timing:
- min_time, max_time = map(float, timing.split('-'))
- elif ',' in timing:
- min_time, max_time = map(float, timing.split(','))
- else:
- # Single value, use it as both min and max
- min_time = max_time = float(timing)
- else:
- # If timing is a number, use it as both min and max
- min_time = max_time = float(timing)
+ return random.uniform(*default_range)
+ min_time, max_time = parse_time_range(timing)
return random.uniform(min_time, max_time)
- except (ValueError, TypeError, AttributeError):
- # Return default value if any error occurs
- return random.uniform(0.5, 1.5)
\ No newline at end of file
+ except Exception as e:
+ logger.warning(f"Error getting wait time for {timing_key}: {e}")
+ return random.uniform(*default_range)
\ No newline at end of file