src/Customization/Export-XrmRibbon.ps1

<#
    .SYNOPSIS
    Export the ribbon customization XML for a table.

    .DESCRIPTION
    Export the RibbonDiffXml for a specific table by creating a temporary solution containing the table,
    exporting the solution, extracting the customizations.xml, and parsing the RibbonDiffXml node.
    This allows reading and modifying classic ribbon customizations programmatically.

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

    .PARAMETER EntityLogicalName
    Logical name of the table whose ribbon to export.

    .PARAMETER SolutionUniqueName
    Existing solution unique name containing the table. If provided, exports from this solution instead of creating a temporary one.

    .PARAMETER OutputPath
    Folder path where extracted files will be stored. Optional. Defaults to temp folder.

    .OUTPUTS
    System.Xml.XmlElement. The RibbonDiffXml node for the specified entity.

    .EXAMPLE
    $ribbonXml = Export-XrmRibbon -EntityLogicalName "account";
    $ribbonXml = Export-XrmRibbon -EntityLogicalName "contact" -SolutionUniqueName "MySolution";

    .LINK
    https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/customize-commands-ribbon
#>

function Export-XrmRibbon {
    [CmdletBinding()]
    [OutputType([System.Xml.XmlElement])]
    param
    (
        [Parameter(Mandatory = $false, ValueFromPipeline)]
        [Microsoft.PowerPlatform.Dataverse.Client.ServiceClient]
        $XrmClient = $Global:XrmClient,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]
        $EntityLogicalName,

        [Parameter(Mandatory = $false)]
        [string]
        $SolutionUniqueName,

        [Parameter(Mandatory = $false)]
        [string]
        $OutputPath = $env:TEMP
    )
    begin {
        $StopWatch = [System.Diagnostics.Stopwatch]::StartNew();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Start -Parameters ($MyInvocation.MyCommand.Parameters);
    }
    process {
        $useTempSolution = (-not $PSBoundParameters.ContainsKey('SolutionUniqueName'));
        $tempSolutionName = "RibbonExport_$([Guid]::NewGuid().ToString('N').Substring(0, 8))";

        if ($useTempSolution) {
            # Create temporary solution
            $publisher = Get-XrmPublisher | Select-Object -First 1;
            $publisherRef = $publisher.Reference;
            $SolutionUniqueName = $tempSolutionName;

            New-XrmSolution -DisplayName $tempSolutionName -UniqueName $tempSolutionName -PublisherReference $publisherRef | Out-Null;

            # Add entity to solution (ComponentType 1 = Entity)
            $entityMetadata = Get-XrmEntityMetadata -LogicalName $EntityLogicalName;
            Add-XrmSolutionComponent -SolutionUniqueName $tempSolutionName -ComponentId $entityMetadata.MetadataId -ComponentType 1 -DoNotIncludeSubcomponents $true | Out-Null;
        }

        try {
            # Export solution
            $solutionFilePath = Export-XrmSolution -SolutionUniqueName $SolutionUniqueName -Managed $false -ExportPath $OutputPath;

            # Extract zip
            $extractPath = Join-Path $OutputPath "RibbonExtract_$([Guid]::NewGuid().ToString('N').Substring(0, 8))";
            Expand-Archive -Path $solutionFilePath -DestinationPath $extractPath -Force;

            # Parse customizations.xml
            $customizationsPath = Join-Path $extractPath "customizations.xml";
            if (-not (Test-Path $customizationsPath)) {
                throw "customizations.xml not found in exported solution.";
            }

            [xml]$customizationsXml = Get-Content -Path $customizationsPath -Raw;

            # Find the entity node
            $entityNode = $customizationsXml.ImportExportXml.Entities.Entity | Where-Object {
                $_.EntityInfo.entity.Name -eq $EntityLogicalName;
            };

            if (-not $entityNode) {
                throw "Entity '$EntityLogicalName' not found in customizations.xml.";
            }

            $ribbonDiffXml = $entityNode.RibbonDiffXml;
            $ribbonDiffXml;
        }
        finally {
            # Cleanup temp solution
            if ($useTempSolution) {
                $tempSolution = Get-XrmSolution -SolutionUniqueName $tempSolutionName;
                if ($tempSolution) {
                    Uninstall-XrmSolution -SolutionUniqueName $tempSolutionName;
                }
            }

            # Cleanup extracted files
            if ((Test-Path $extractPath)) {
                Remove-Item -Path $extractPath -Recurse -Force -ErrorAction SilentlyContinue;
            }
        }
    }
    end {
        $StopWatch.Stop();
        Trace-XrmFunction -Name $MyInvocation.MyCommand.Name -Stage Stop -StopWatch $StopWatch;
    }
}

Export-ModuleMember -Function Export-XrmRibbon -Alias *;

Register-ArgumentCompleter -CommandName Export-XrmRibbon -ParameterName "EntityLogicalName" -ScriptBlock {
    param($CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters)
    $validLogicalNames = Get-XrmEntitiesLogicalName;
    return $validLogicalNames | Where-Object { $_ -like "$wordToComplete*" };
}