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)

    .PARAMETER OnlyIfEmpty
    Keep the solution, without error, when it still has components (an unmanaged solution used as a container).

    .PARAMETER IfExists
    Do nothing, without error, when the solution does not exist.

    .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;

    .EXAMPLE
    # Idempotent cleanup of a temporary container solution (alias Remove-XrmSolution)
    Remove-XrmSolution -XrmClient $xrmClient -SolutionUniqueName "contoso_temp" -OnlyIfEmpty -IfExists;

    .LINK
    https://github.com/AymericM78/PowerDataOps/blob/main/documentation/commands/Uninstall-XrmSolution.md
#>

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,

        [Parameter(Mandatory = $false)]
        [switch]
        $OnlyIfEmpty,

        [Parameter(Mandatory = $false)]
        [switch]
        $IfExists
    )
    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) {
            if ($IfExists) {
                return;
            }
            throw "Solution '$SolutionUniqueName' not found!";
        }
        if ($OnlyIfEmpty) {
            $components = @(Get-XrmSolutionComponents -XrmClient $XrmClient -SolutionUniqueName $SolutionUniqueName -IfExists);
            if ($components.Count -gt 0) {
                Write-Verbose "Solution '$SolutionUniqueName' kept: it has $($components.Count) component(s).";
                return;
            }
        }

        $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;
    }
}

Set-Alias -Name Remove-XrmSolution -Value Uninstall-XrmSolution;
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;
}