private/Invoke-SudoCommand.ps1

function Invoke-SudoCommand {
    <#
    .SYNOPSIS
        Executes a command with sudo if required on Linux.
 
    .DESCRIPTION
        Wraps command execution to handle sudo requirements on Linux.
        On macOS and Windows, runs commands directly without modification.
 
        For Linux with LocalMachine scope:
        - Validates sudo access is available before execution
        - Prepends sudo to commands if not running as root
        - Provides clear error messages if sudo access fails
 
    .PARAMETER Command
        The command to execute.
 
    .PARAMETER Scope
        The installation scope. Affects whether sudo is needed.
 
    .PARAMETER Description
        A description of what the command does, for error messages.
 
    .OUTPUTS
        [PSCustomObject] With properties:
        - Success: [bool] Whether the command succeeded
        - ExitCode: [int] The exit code
        - Output: [string] Combined stdout/stderr
        - Command: [string] The actual command that was run
 
    .EXAMPLE
        Invoke-SudoCommand -Command 'apt-get update' -Scope LocalMachine -Description 'updating package lists'
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Command,

        [Parameter()]
        [ValidateSet('CurrentUser', 'LocalMachine')]
        [string]$Scope = 'CurrentUser',

        [Parameter()]
        [string]$Description = 'executing command'
    )

    $os = Get-OperatingSystem
    $needsSudo = Test-SudoRequired -Scope $Scope
    $actualCommand = $Command

    # On Linux with LocalMachine scope, we need to handle sudo
    if ($needsSudo) {
        Write-PSFMessage -Level Verbose -Message "Sudo required for: $Description"

        # First, validate that sudo access is available
        # Use sudo -n (non-interactive) to check if we have passwordless sudo
        # or if sudo credentials are cached
        $sudoCheck = & bash -c 'sudo -n true 2>/dev/null; echo $?' 2>$null
        $hasSudoAccess = ($sudoCheck -eq '0')

        if (-not $hasSudoAccess) {
            # Try to prompt for sudo password by running sudo -v
            # This will cache credentials for subsequent commands
            Write-PSFMessage -Level Host -Message "Elevated privileges required for $Description. You may be prompted for your password."

            # Run sudo -v to prompt for password and cache credentials
            # This needs to be interactive, so we use Start-Process with UseShellExecute
            $validateResult = & bash -c 'sudo -v 2>&1; echo "EXIT:$?"'
            $exitLine = $validateResult | Select-Object -Last 1
            $sudoValidated = $exitLine -eq 'EXIT:0'

            if (-not $sudoValidated) {
                return [PSCustomObject]@{
                    Success  = $false
                    ExitCode = 1
                    Output   = "Failed to obtain sudo privileges. Please ensure you have sudo access and try again."
                    Command  = $Command
                }
            }
        }

        # Prepend sudo to the command if it doesn't already have it
        if ($Command -notmatch '^\s*sudo\s') {
            $actualCommand = "sudo $Command"
        }
    }

    Write-PSFMessage -Level Verbose -Message "Executing: $actualCommand"

    try {
        if ($os -eq 'Windows') {
            # On Windows, use cmd.exe
            $psi = New-Object System.Diagnostics.ProcessStartInfo
            $psi.FileName = 'cmd.exe'
            $psi.Arguments = "/c `"$actualCommand`""
            $psi.RedirectStandardOutput = $true
            $psi.RedirectStandardError = $true
            $psi.UseShellExecute = $false
            $psi.CreateNoWindow = $true
        } else {
            # On Linux/macOS, use bash
            $psi = New-Object System.Diagnostics.ProcessStartInfo
            $psi.FileName = '/bin/bash'
            $psi.Arguments = "-c `"$actualCommand`""
            $psi.RedirectStandardOutput = $true
            $psi.RedirectStandardError = $true
            $psi.UseShellExecute = $false
            $psi.CreateNoWindow = $true
        }

        $process = New-Object System.Diagnostics.Process
        $process.StartInfo = $psi
        $process.Start() | Out-Null

        $stdout = $process.StandardOutput.ReadToEnd()
        $stderr = $process.StandardError.ReadToEnd()
        $process.WaitForExit()

        $output = @($stdout, $stderr) | Where-Object { $_ } | Join-String -Separator "`n"

        return [PSCustomObject]@{
            Success  = ($process.ExitCode -eq 0)
            ExitCode = $process.ExitCode
            Output   = $output
            Command  = $actualCommand
        }
    } catch {
        return [PSCustomObject]@{
            Success  = $false
            ExitCode = -1
            Output   = $_.Exception.Message
            Command  = $actualCommand
        }
    }
}