src/Solutions/Uninstall-XrmSolution.ps1

<#
    .SYNOPSIS
    Uninstall a solution from Microsoft Dataverse.

    .DESCRIPTION
    Delete a solution (managed or unmanaged) from the environment by its unique name.
    Uses the UninstallSolutionAsync SDK message to avoid timeout issues, then monitors
    the async operation via Watch-XrmAsynchOperation until completion.
    Raises an error when the uninstall system job fails or is canceled.

    .PARAMETER XrmClient
    Xrm connector initialized to target instance. Use latest one by default. (Dataverse ServiceClient)

    .PARAMETER SolutionUniqueName
    Solution unique name to uninstall.

    .PARAMETER PassThru
    Return the status of the uninstall system job (see Watch-XrmAsynchOperation). (Default: nothing is returned)

    .OUTPUTS
    PSCustomObject. With PassThru only: Id, StatusCode, Status, Message, FriendlyMessage of the uninstall system job.

    .EXAMPLE
    Uninstall-XrmSolution -SolutionUniqueName "contoso_crm";

    .EXAMPLE
    $status = Uninstall-XrmSolution -XrmClient $xrmClient -SolutionUniqueName "contoso_crm" -PassThru;

    .LINK
    https://learn.microsoft.com/en-us/power-apps/developer/data-platform/uninstall-delete-solution
#>

function Uninstall-XrmSolution {
    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([PSCustomObject])]
    param
    (
        [Parameter(Mandatory = $false, ValueFromPipeline)]
        [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]
        $XrmClient = $Global:XrmClient,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $SolutionUniqueName,

        [Parameter(Mandatory = $false)]
        [switch]
        $PassThru
    )
    begin {
        $StopWatch = [System.Diagnostics.Stopwatch]::StartNew();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Start -Parameters ($MyInvocation.MyCommand.Parameters);
    }
    process {
        $solution = $XrmClient | Get-XrmSolution -SolutionUniqueName $SolutionUniqueName -Columns @("solutionid", "uniquename");
        if (-not $solution) {
            throw "Solution '$SolutionUniqueName' not found!";
        }

        $uninstallRequest = New-XrmRequest -Name "UninstallSolutionAsync";
        $uninstallRequest = $uninstallRequest | Add-XrmRequestParameter -Name "SolutionUniqueName" -Value $SolutionUniqueName;

        try {
            $response = $XrmClient | Invoke-XrmRequest -Request $uninstallRequest;
            if ($WhatIfPreference -and $null -eq $response) { return; }
            $asyncOperationId = $response.Results["AsyncOperationId"];
            $uninstallStatus = $XrmClient | Watch-XrmAsynchOperation -AsyncOperationId $asyncOperationId -MissingMeansSucceeded -ThrowOnFailure -ScriptBlock {
                param($asyncOperation)

                Write-HostAndLog " > Uninstalling '$SolutionUniqueName' solution : Asyncoperation $($asyncOperation.Id) | Status = $($asyncOperation.statuscode)" -ForegroundColor Cyan;
            };
        }
        catch {
            $errorMessage = $_.Exception.Message;
            Write-HostAndLog "$($MyInvocation.MyCommand.Name) => KO : [Error: $errorMessage]" -ForegroundColor Red -Level FAIL;
            throw $errorMessage;
        }

        if ($PassThru) {
            $uninstallStatus;
        }
    }
    end {
        $StopWatch.Stop();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Stop -StopWatch $StopWatch;
    }
}

Export-ModuleMember -Function Uninstall-XrmSolution -Alias *;

Register-ArgumentCompleter -CommandName Uninstall-XrmSolution -ParameterName "SolutionUniqueName" -ScriptBlock {

    param($CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters)

    $solutionUniqueNames = @();
    $solutions = Get-XrmSolutions -Columns "uniquename";
    $solutions | ForEach-Object { $solutionUniqueNames += $_.uniquename };
    return $solutionUniqueNames | Where-Object { $_ -like "$wordToComplete*" } | Sort-Object;
}