Private/Add-OSDAppMicrosoft365Apps.ps1

function Add-OSDAppMicrosoft365AppsInternal {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [string]$CachePath,
        [Parameter(Mandatory)][string]$WindowsPath,
        [ValidateSet('Current','MonthlyEnterprise','SemiAnnual','CurrentPreview','SemiAnnualPreview','BetaChannel')]
        [string]$Channel = 'Current',
        [ValidateSet('64','32')][string]$Architecture = '64',
        [ValidateSet('O365ProPlusRetail','O365BusinessRetail')][string]$ProductId = 'O365ProPlusRetail',
        [string[]]$Language = @('en-us'),
        [bool]$AcceptEula = $true,
        [bool]$SharedComputerLicensing = $false,
        [bool]$DeviceBasedLicensing = $false,
        [ValidateSet('Access','Excel','Groove','Lync','OneDrive','OneNote','Outlook','OutlookForWindows','PowerPoint','Publisher','Teams','Word')]
        [string[]]$ExcludeApp,
        [string]$ConfigurationXml,
        [bool]$UseCachedConfiguration = $true,
        [string]$OfficeDeploymentToolUri = 'https://officecdn.microsoft.com/pr/wsus/setup.exe',
        [string]$StagedRelativePath = 'Windows\Temp\OSDApps'
    )

    if ($SharedComputerLicensing -and $DeviceBasedLicensing) {
        throw 'SharedComputerLicensing and DeviceBasedLicensing cannot both be enabled.'
    }

    $destinationRoot = Join-Path $WindowsPath $StagedRelativePath
    $destinationBuiltIn = Join-Path $destinationRoot 'BuiltIn\Microsoft365Apps'
    $deviceManifestPath = Join-Path $destinationRoot 'DeviceManifest.json'
    $clientLogPath = if ($CachePath) { Join-Path $CachePath 'Logs\Client.log' } else { Join-Path $WindowsPath 'ProgramData\OSDApps\Logs\Client.log' }

    $cacheRoot = if ($CachePath) { Join-Path $CachePath 'BuiltIn\Microsoft365Apps' } else { $null }
    $cacheConfig = if ($cacheRoot) { Join-Path $cacheRoot 'configuration.xml' } else { $null }
    $cacheHasPayload = $false
    if ($cacheRoot) {
        $cacheHasPayload = (
            (Test-Path -LiteralPath (Join-Path $cacheRoot 'Office\Data') -PathType Container) -and
            (Test-Path -LiteralPath (Join-Path $cacheRoot 'setup.exe') -PathType Leaf) -and
            (Test-Path -LiteralPath $cacheConfig -PathType Leaf)
        )
    }

    if (-not $PSCmdlet.ShouldProcess($destinationBuiltIn, 'Stage Microsoft 365 Apps deployment intent and available cache')) { return }

    if (Test-Path -LiteralPath $destinationBuiltIn) {
        Remove-Item -LiteralPath $destinationBuiltIn -Recurse -Force
    }
    New-Item -ItemType Directory -Path $destinationBuiltIn -Force | Out-Null

    if ($cacheHasPayload) {
        Copy-Item -Path (Join-Path $cacheRoot '*') -Destination $destinationBuiltIn -Recurse -Force
    }

    $configPath = Join-Path $destinationBuiltIn 'configuration.xml'

    if ($ConfigurationXml) {
        if (-not (Test-Path -LiteralPath $ConfigurationXml -PathType Leaf)) {
            throw "Office configuration XML not found: $ConfigurationXml"
        }
        Copy-Item -LiteralPath $ConfigurationXml -Destination $configPath -Force
    }
    elseif ($UseCachedConfiguration -and $cacheHasPayload -and (Test-Path -LiteralPath $cacheConfig -PathType Leaf)) {
        Copy-Item -LiteralPath $cacheConfig -Destination $configPath -Force
    }
    else {
        if (-not $Language -or @($Language).Count -eq 0) { throw 'At least one Office language must be specified.' }
        $settings = New-Object System.Xml.XmlWriterSettings
        $settings.Indent = $true
        $settings.Encoding = New-Object System.Text.UTF8Encoding($false)
        $writer = [System.Xml.XmlWriter]::Create($configPath, $settings)
        try {
            $writer.WriteStartDocument()
            $writer.WriteStartElement('Configuration')
            $writer.WriteStartElement('Add')
            $writer.WriteAttributeString('OfficeClientEdition', $Architecture)
            $writer.WriteAttributeString('Channel', $Channel)
            $writer.WriteStartElement('Product')
            $writer.WriteAttributeString('ID', $ProductId)
            foreach ($culture in $Language) {
                if ([string]::IsNullOrWhiteSpace($culture)) { continue }
                $writer.WriteStartElement('Language'); $writer.WriteAttributeString('ID', $culture); $writer.WriteEndElement()
            }
            foreach ($app in @($ExcludeApp)) {
                if ([string]::IsNullOrWhiteSpace($app)) { continue }
                $writer.WriteStartElement('ExcludeApp'); $writer.WriteAttributeString('ID', $app); $writer.WriteEndElement()
            }
            $writer.WriteEndElement()
            $writer.WriteEndElement()
            $writer.WriteStartElement('Display')
            $writer.WriteAttributeString('Level', 'None')
            $writer.WriteAttributeString('AcceptEULA', $(if ($AcceptEula) { 'TRUE' } else { 'FALSE' }))
            $writer.WriteEndElement()
            if ($SharedComputerLicensing) {
                $writer.WriteStartElement('Property'); $writer.WriteAttributeString('Name', 'SharedComputerLicensing'); $writer.WriteAttributeString('Value', '1'); $writer.WriteEndElement()
            }
            if ($DeviceBasedLicensing) {
                $writer.WriteStartElement('Property'); $writer.WriteAttributeString('Name', 'DeviceBasedLicensing'); $writer.WriteAttributeString('Value', '1'); $writer.WriteEndElement()
            }
            $writer.WriteStartElement('Updates'); $writer.WriteAttributeString('Enabled', 'TRUE'); $writer.WriteEndElement()
            $writer.WriteEndElement()
            $writer.WriteEndDocument()
        }
        finally { $writer.Dispose() }
    }

    if (Test-Path -LiteralPath $deviceManifestPath -PathType Leaf) {
        $deviceManifest = Get-Content -LiteralPath $deviceManifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
    } else {
        New-Item -ItemType Directory -Path $destinationRoot -Force | Out-Null
        $deviceManifest = [pscustomobject]@{ SchemaVersion='1.0'; StagedAt=(Get-Date).ToUniversalTime().ToString('o'); Apps=@() }
    }

    $manifestApp = [pscustomobject]@{
        Source = 'BuiltIn'
        Id = 'Microsoft365Apps'
        DisplayName = 'Microsoft 365 Apps'
        Type = 'OfficeDeploymentTool'
        Configuration = 'BuiltIn\Microsoft365Apps\configuration.xml'
        Setup = 'BuiltIn\Microsoft365Apps\setup.exe'
        Offline = $true
        Architecture = $Architecture
        Channel = $Channel
        ProductId = $ProductId
        Language = @($Language)
        AcceptEula = $AcceptEula
        SharedComputerLicensing = $SharedComputerLicensing
        DeviceBasedLicensing = $DeviceBasedLicensing
        ExcludeApp = @($ExcludeApp)
        OfficeDeploymentToolUri = $OfficeDeploymentToolUri
        CachePreferred = [bool]$CachePath
    }

    $deviceManifest = Set-OSDAppManifestApp -Manifest $deviceManifest -App $manifestApp
    $deviceManifest.StagedAt = (Get-Date).ToUniversalTime().ToString('o')
    $deviceManifest | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $deviceManifestPath -Encoding UTF8

    Write-OSDAppLog -LogPath $clientLogPath -Component 'Microsoft365Apps' -Event 'CacheDetection' -Message $(if ($CachePath) { 'OSDCloud USB cache detected.' } else { 'No OSDCloud USB cache detected. Direct local acquisition will be used during SetupComplete.' }) -Data @{ CachePath=$CachePath; CacheAvailable=$cacheHasPayload }
    Write-OSDAppLog -LogPath $clientLogPath -Component 'Microsoft365Apps' -Event 'StageTarget' -Message 'Microsoft 365 Apps deployment intent staged to the OS disk.' -Data @{ Destination=$destinationBuiltIn; AcquisitionPhase='SetupComplete'; CachePreferred=[bool]$CachePath }
    Write-OSDAppLog -LogPath $clientLogPath -Component 'Microsoft365Apps' -Event 'OfficeStageComplete' -Message 'Microsoft 365 Apps deployment intent staged.' -Data @{ Destination=$destinationBuiltIn; CacheAvailable=$cacheHasPayload; CachePath=$CachePath }

    [pscustomobject]@{
        PSTypeName='OSDApps.StagedApp'
        Name='Microsoft365Apps'
        CachePath=$CachePath
        WindowsPath=$WindowsPath
        StagedPath=$destinationBuiltIn
        Source='BuiltIn'
        CacheAvailable=$cacheHasPayload
    }
}