1111
1212import asyncio
1313import shutil
14+ import subprocess
1415import sys
1516from importlib .metadata import PackageNotFoundError
1617from importlib .metadata import version as get_version
@@ -156,6 +157,30 @@ def __init__(
156157 repo = GITHUB_REPO ,
157158 session = session ,
158159 )
160+ self ._uv_available : bool = self ._check_uv_available ()
161+
162+ def _check_uv_available (self ) -> bool :
163+ """Check if UV is available in the system.
164+
165+ Returns:
166+ True if UV is installed and available, False otherwise
167+
168+ """
169+ try :
170+ result = subprocess .run (
171+ ["uv" , "--version" ],
172+ check = False ,
173+ capture_output = True ,
174+ text = True ,
175+ timeout = 5 ,
176+ )
177+ available = result .returncode == 0
178+ if available :
179+ logger .debug ("UV is available: %s" , result .stdout .strip ())
180+ return available
181+ except (FileNotFoundError , subprocess .TimeoutExpired ):
182+ logger .debug ("UV is not available in PATH" )
183+ return False
159184
160185 def get_current_version (self ) -> str :
161186 """Get the current installed version of my-unicorn.
@@ -343,55 +368,52 @@ async def check_for_update(self, refresh_cache: bool = False) -> bool:
343368 return False
344369
345370 async def perform_update (self ) -> bool :
346- """Update by doing a fresh git clone and running the installer.
371+ """Update by cloning repo and running setup.sh install.
372+
373+ Simplified approach that delegates all
374+ installation logic to setup.sh, eliminating code duplication.
347375
348376 Returns:
349377 True if update was successful, False otherwise
350378
351379 """
352380 repo_dir = self .global_config ["directory" ]["repo" ]
353- package_dir = self .global_config ["directory" ]["package" ]
354- installer = package_dir / "setup.sh"
381+ installer_script = repo_dir / "setup.sh"
355382
356383 logger .debug ("Starting upgrade to my-unicorn..." )
357384 logger .debug ("Repository directory: %s" , repo_dir )
358- logger .debug ("Package directory: %s" , package_dir )
359- logger .debug ("Installer script: %s" , installer )
385+ logger .debug ("UV available: %s" , self ._uv_available )
360386
361- # Track progress with simple indicators
387+ # Inform user about UV usage
388+ if self ._uv_available :
389+ logger .info ("UV detected - will use UV for faster installation" )
390+ else :
391+ logger .info (
392+ "Using pip for installation (install UV for faster updates)"
393+ )
394+
395+ # Track progress
362396 download_task_id = None
363- file_task_id = None
364397 install_task_id = None
365398 cleanup_task_id = None
366399
367400 try :
368401 # 1) Prepare fresh repo directory
369402 if repo_dir .exists ():
370403 logger .info ("Removing old repo at %s" , repo_dir )
371- logger .debug (
372- "Old repo directory size: %s" ,
373- repo_dir .stat ().st_size
374- if repo_dir .is_file ()
375- else "directory" ,
376- )
377404 shutil .rmtree (repo_dir )
378405 logger .debug ("Old repo directory removed successfully" )
406+
379407 repo_dir .mkdir (parents = True )
380408 logger .debug ("Created fresh repo directory: %s" , repo_dir )
381409
382- # 2) Clone into repo_dir with simple progress tracking
410+ # 2) Clone repository
383411 if self .progress :
384412 download_task_id = self .progress .start_task (
385413 "repo_clone" , "Cloning repository from GitHub..."
386414 )
387415
388416 logger .info ("Cloning repository to %s" , repo_dir )
389- logger .debug (
390- "Git clone command: git clone %s %s" ,
391- f"{ GITHUB_URL } .git" ,
392- str (repo_dir ),
393- )
394-
395417 clone_process = await asyncio .create_subprocess_exec (
396418 "git" ,
397419 "clone" ,
@@ -401,40 +423,15 @@ async def perform_update(self) -> bool:
401423 stderr = asyncio .subprocess .PIPE ,
402424 )
403425
404- # Simple progress updates during clone
426+ # Progress updates during clone
405427 clone_task = asyncio .create_task (clone_process .wait ())
406428 if self .progress and download_task_id :
407- # Show progress during clone
408429 while not clone_task .done ():
409430 self .progress .update_task (download_task_id )
410431 await asyncio .sleep (0.5 )
411432
412433 await clone_task
413434
414- logger .debug (
415- "Git clone process completed with return code: %s" ,
416- clone_process .returncode ,
417- )
418- if clone_process .returncode == 0 :
419- logger .debug ("Repository cloned successfully to %s" , repo_dir )
420- # Check if repo directory has expected content
421- try :
422- repo_contents = list (repo_dir .iterdir ())
423- logger .debug (
424- "Cloned repository contains %d items" ,
425- len (repo_contents ),
426- )
427- logger .debug (
428- "Repository structure: %s" ,
429- [item .name for item in repo_contents ],
430- )
431- except Exception as e :
432- logger .debug (
433- "Could not list repo directory contents: %s" , e
434- )
435- else :
436- logger .debug ("Git clone failed, will handle error" )
437-
438435 if self .progress and download_task_id :
439436 self .progress .finish_task (
440437 download_task_id ,
@@ -445,151 +442,57 @@ async def perform_update(self) -> bool:
445442 )
446443
447444 if clone_process .returncode != 0 :
448- logger .error (
449- "Git clone failed with return code %s" ,
450- clone_process .returncode ,
451- )
445+ logger .error ("Git clone failed" )
452446 print ("❌ Failed to download repository" )
453447 return False
454448
455- # 3) Copy files with simple progress tracking
456- if self .progress :
457- file_task_id = self .progress .start_task (
458- "file_operations" , "Copying project files..."
459- )
460-
461- logger .info ("Copying code + scripts to %s" , package_dir )
462- logger .debug ("Ensuring package directory exists: %s" , package_dir )
463- package_dir .mkdir (parents = True , exist_ok = True )
464- logger .debug ("Package directory created/verified" )
465-
466- # Copy over the package code, scripts, and installation files
467- files_to_copy = (
468- "my_unicorn" ,
469- "scripts" ,
470- "pyproject.toml" ,
471- "setup.sh" ,
472- )
473- for name in files_to_copy :
474- src = repo_dir / name
475- dst = package_dir / name
476-
477- logger .debug ("Processing file/directory: %s" , name )
478- logger .debug ("Source: %s" , src )
479- logger .debug ("Destination: %s" , dst )
480-
481- if not src .exists ():
482- logger .warning ("Source file/directory not found: %s" , src )
483- logger .debug ("Skipping %s - source does not exist" , name )
484- continue
485-
486- if self .progress and file_task_id :
487- self .progress .update_task (
488- file_task_id ,
489- description = f"Copying { name } ..." ,
490- )
491-
492- # Remove the old directory/file
493- # (but preserve venv and other dirs)
494- if dst .exists ():
495- logger .debug ("Destination exists, removing old: %s" , dst )
496- if dst .is_dir ():
497- logger .debug ("Removing old directory: %s" , dst )
498- shutil .rmtree (dst )
499- else :
500- logger .debug ("Removing old file: %s" , dst )
501- dst .unlink ()
502-
503- # Copy fresh
504- if src .is_dir ():
505- logger .debug (
506- "Copying directory tree from %s to %s" , src , dst
507- )
508- _ = shutil .copytree (src , dst )
509- logger .debug ("Directory copy completed for %s" , name )
510- else :
511- logger .debug ("Copying file from %s to %s" , src , dst )
512- _ = shutil .copy2 (src , dst )
513- logger .debug ("File copy completed for %s" , name )
514-
515- if self .progress and file_task_id :
516- self .progress .finish_task (
517- file_task_id ,
518- success = True ,
519- final_description = "Files copied successfully" ,
449+ # 3) Run setup.sh install from the cloned repo
450+ if not installer_script .exists ():
451+ logger .error (
452+ "Installer script missing at %s" , installer_script
520453 )
521-
522- # 4) Run installer with simple progress tracking
523- if not installer .exists ():
524- logger .error ("Installer script missing at %s" , installer )
525454 print ("❌ Installer script not found." )
526455 return False
527456
528- # Make installer executable
529- logger .debug ("Setting installer script permissions: %s" , installer )
530- installer .chmod (0o755 )
531- logger .debug ("Installer script is now executable" )
457+ logger .debug ("Making installer executable: %s" , installer_script )
458+ installer_script .chmod (0o755 )
532459
533460 if self .progress :
461+ install_msg = (
462+ "Running installation with UV..."
463+ if self ._uv_available
464+ else "Running installation with pip..."
465+ )
534466 install_task_id = self .progress .start_task (
535467 "upgrade_installation" ,
536- "Running installer in UPDATE mode..." ,
468+ install_msg ,
537469 )
538470
539- logger .debug ("Starting installer subprocess" )
540- logger .debug ("Command: bash %s update" , str (installer ))
541- logger .debug ("Working directory: %s" , str (package_dir ))
471+ logger .info ("Executing: bash %s install" , installer_script )
542472 install_process = await asyncio .create_subprocess_exec (
543473 "bash" ,
544- str (installer ),
545- "update " ,
546- cwd = str (package_dir ),
474+ str (installer_script ),
475+ "install " ,
476+ cwd = str (repo_dir ),
547477 stdout = asyncio .subprocess .PIPE ,
548478 stderr = asyncio .subprocess .STDOUT ,
549479 )
550- logger .debug (
551- "Installer subprocess started with PID: %s" ,
552- install_process .pid ,
553- )
554480
555- # Stream output and update progress
481+ # Stream output with progress updates
556482 if install_process .stdout :
557483 while True :
558484 line = await install_process .stdout .readline ()
559485 if not line :
560486 break
561-
562487 line_str = line .decode ().strip ()
563-
564- # Debug log all installer output for troubleshooting
565488 logger .debug ("Installer output: %s" , line_str )
566489
567- # Detailed logging for specific venv and
568- # installation operations
569- if "virtual environment" in line_str .lower ():
570- logger .debug ("VENV OPERATION: %s" , line_str )
571- elif "pip install" in line_str .lower ():
572- logger .debug ("PIP INSTALL: %s" , line_str )
573- elif "installing" in line_str .lower ():
574- logger .debug ("PACKAGE INSTALL: %s" , line_str )
575- elif "creating" in line_str .lower ():
576- logger .debug ("CREATION STEP: %s" , line_str )
577- elif "updating" in line_str .lower ():
578- logger .debug ("UPDATE STEP: %s" , line_str )
579- elif "complete" in line_str .lower ():
580- logger .debug ("COMPLETION: %s" , line_str )
581- elif "error" in line_str .lower ():
582- logger .debug ("ERROR OUTPUT: %s" , line_str )
583- elif "warning" in line_str .lower ():
584- logger .debug ("WARNING OUTPUT: %s" , line_str )
585-
586- # Update progress indicator on key installer stages
587490 if (
588491 self .progress
589492 and install_task_id
590493 and any (
591- keyword in line_str .lower ()
592- for keyword in [
494+ kw in line_str .lower ()
495+ for kw in [
593496 "creating" ,
594497 "installing" ,
595498 "updating" ,
@@ -599,17 +502,8 @@ async def perform_update(self) -> bool:
599502 ):
600503 self .progress .update_task (install_task_id )
601504
602- _ = await install_process .wait ()
603-
604- logger .debug ("Installer process completed" )
505+ await install_process .wait ()
605506 logger .debug ("Installer exit code: %s" , install_process .returncode )
606- if install_process .returncode == 0 :
607- logger .debug ("Installation successful" )
608- else :
609- logger .debug (
610- "Installation failed with code: %s" ,
611- install_process .returncode ,
612- )
613507
614508 if self .progress and install_task_id :
615509 self .progress .finish_task (
@@ -627,29 +521,14 @@ async def perform_update(self) -> bool:
627521 )
628522 return False
629523
630- # 5 ) Clean up repo_dir with simple progress tracking
524+ # 4 ) Clean up cloned repository
631525 if self .progress :
632526 cleanup_task_id = self .progress .start_task (
633527 "cleanup" , "Cleaning up temporary files..."
634528 )
635529
636530 if repo_dir .exists ():
637531 logger .info ("Cleaning up repo directory" )
638- logger .debug ("Removing repo directory: %s" , repo_dir )
639- try :
640- dir_size = sum (
641- f .stat ().st_size
642- for f in repo_dir .rglob ("*" )
643- if f .is_file ()
644- )
645- logger .debug (
646- "Repo directory size before cleanup: %d bytes" ,
647- dir_size ,
648- )
649- except Exception as e :
650- logger .debug (
651- "Could not calculate repo directory size: %s" , e
652- )
653532 shutil .rmtree (repo_dir )
654533 logger .debug ("Repo directory cleanup completed" )
655534
@@ -660,18 +539,13 @@ async def perform_update(self) -> bool:
660539 final_description = "Cleanup completed" ,
661540 )
662541
663- logger .debug ("Self-update completed successfully" )
664- logger .debug (
665- "All operations completed: clone, file copy, "
666- "installation, cleanup"
667- )
542+ logger .info ("Self-update completed successfully" )
668543 return True
669544
670545 except Exception as e :
671- # Finish any pending progress tasks with error
546+ # Finish any pending progress tasks
672547 for task_id in [
673548 download_task_id ,
674- file_task_id ,
675549 install_task_id ,
676550 cleanup_task_id ,
677551 ]:
0 commit comments