@@ -716,6 +716,135 @@ def apply_updates(self) -> tuple[bool, Optional[str]]:
716716 return False , error_msg
717717
718718
719+ class PkgAddPackageManager (PackageManager ):
720+ """Package manager for OpenBSD pkg_add."""
721+
722+ def refresh_cache (self ) -> bool :
723+ """Refresh pkg_add cache (not really needed for OpenBSD, but check connection)."""
724+ # OpenBSD pkg_add doesn't have a cache refresh like other systems
725+ # But we can verify the package database is accessible
726+ try :
727+ exit_code , stdout , stderr = self .connection .execute_command (
728+ 'pkg_info -Q ""' , timeout = 30
729+ )
730+ # Any exit code is fine here, we're just checking connection
731+ return True
732+ except Exception as e :
733+ logger .error (f"Failed to check pkg_add availability: { e } " )
734+ return False
735+
736+ def check_updates (self ) -> List [PackageUpdate ]:
737+ """Check for pkg_add package updates."""
738+ updates = []
739+
740+ try :
741+ # Use pkg_add -u with -n (dry-run) to see what would be updated
742+ exit_code , stdout , stderr = self .connection .execute_command (
743+ 'doas pkg_add -u -n' , timeout = 120
744+ )
745+
746+ if exit_code != 0 :
747+ logger .warning (f"pkg_add check command failed: { stderr } " )
748+ return updates
749+
750+ updates = self ._parse_pkg_add_output (stdout )
751+
752+ except Exception as e :
753+ logger .error (f"Failed to check pkg_add updates: { e } " )
754+
755+ return updates
756+
757+ def _parse_pkg_add_output (self , output : str ) -> List [PackageUpdate ]:
758+ """Parse pkg_add -u -n output."""
759+ updates = []
760+
761+ for line in output .strip ().split ('\n ' ):
762+ line = line .strip ()
763+ if not line :
764+ continue
765+
766+ # Format examples:
767+ # "Update to package-1.2.3"
768+ # "quirks-7.14 -> quirks-7.15"
769+ if '->' in line :
770+ # Format: package-old-version -> package-new-version
771+ parts = line .split ('->' )
772+ if len (parts ) >= 2 :
773+ left_part = parts [0 ].strip ()
774+ right_part = parts [1 ].strip ()
775+
776+ # Extract package name and versions
777+ # OpenBSD format: packagename-version
778+ if '-' in left_part :
779+ last_dash = left_part .rfind ('-' )
780+ package_name = left_part [:last_dash ]
781+ current_version = left_part [last_dash + 1 :]
782+ else :
783+ package_name = left_part
784+ current_version = "unknown"
785+
786+ if '-' in right_part :
787+ last_dash = right_part .rfind ('-' )
788+ available_version = right_part [last_dash + 1 :]
789+ else :
790+ available_version = right_part
791+
792+ update = PackageUpdate (
793+ name = package_name ,
794+ current_version = current_version ,
795+ available_version = available_version ,
796+ repository = "OpenBSD"
797+ )
798+ updates .append (update )
799+ elif 'update to' in line .lower ():
800+ # Format: "Update to package-version"
801+ match = re .search (r'update to ([^\s]+)' , line , re .IGNORECASE )
802+ if match :
803+ package_with_version = match .group (1 )
804+ if '-' in package_with_version :
805+ last_dash = package_with_version .rfind ('-' )
806+ package_name = package_with_version [:last_dash ]
807+ available_version = package_with_version [last_dash + 1 :]
808+ else :
809+ package_name = package_with_version
810+ available_version = "unknown"
811+
812+ update = PackageUpdate (
813+ name = package_name ,
814+ current_version = "installed" ,
815+ available_version = available_version ,
816+ repository = "OpenBSD"
817+ )
818+ updates .append (update )
819+
820+ return updates
821+
822+ def apply_updates (self ) -> tuple [bool , Optional [str ]]:
823+ """Apply all available pkg_add updates."""
824+ try :
825+ # OpenBSD uses doas instead of sudo by default
826+ # Apply updates with -u (update) flag
827+ exit_code , stdout , stderr = self .connection .execute_command (
828+ 'doas pkg_add -u' ,
829+ timeout = 1800 # 30 minutes for updates
830+ )
831+
832+ if exit_code == 0 :
833+ logger .info ("Successfully applied pkg_add updates" )
834+ return True , None
835+ else :
836+ error_output = f"pkg_add update failed with exit code { exit_code } \n "
837+ error_output += f"STDOUT:\n { stdout } \n " if stdout .strip () else ""
838+ error_output += f"STDERR:\n { stderr } \n " if stderr .strip () else ""
839+ logger .error (f"Failed to apply pkg_add updates: { stderr } " )
840+ return False , error_output
841+
842+ except Exception as e :
843+ error_msg = f"Error applying pkg_add updates: { e } "
844+ logger .error (error_msg )
845+ return False , error_msg
846+
847+
719848def get_package_manager (connection : SSHConnection , os_info : OSInfo ) -> Optional [PackageManager ]:
720849 """Get appropriate package manager instance for the OS."""
721850 manager_map = {
@@ -725,6 +854,7 @@ def get_package_manager(connection: SSHConnection, os_info: OSInfo) -> Optional[
725854 'zypper' : ZypperPackageManager ,
726855 'pacman' : PackmanPackageManager ,
727856 'pkg' : PkgPackageManager ,
857+ 'pkg_add' : PkgAddPackageManager ,
728858 }
729859
730860 pm_class = manager_map .get (os_info .package_manager )
0 commit comments