private/Initialize-OSDeployBootAssetPath.ps1

#Requires -PSEdition Core

function Initialize-OSDeployBootAssetPath {
    <#
    .SYNOPSIS
        Migrates renamed OSDeploy Boot asset folders and profile paths
 
    .DESCRIPTION
        Migrates legacy shared script and wallpaper folders below Boot-Assets to their
        boot-prefixed names without overwriting destination conflicts. When ProfilePath is
        specified, profile-local script folders and matching saved WinPEScript and MediaScript
        path segments are migrated before profile validation.
 
    .PARAMETER ProfilePath
        Specifies an optional build profile directory containing osdeployboot.json.
 
    .EXAMPLE
        PS> Initialize-OSDeployBootAssetNames -ProfilePath 'C:\ProgramData\OSDeployCore\boot-assets\osdeployboot-profiles\Contoso-amd64'
 
        Migrates shared and Contoso profile content to the current asset folder names.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        None.
 
    .NOTES
        Author: David Segura
        Company: Recast Software
        Version: 1.0.0
        Date: 2026-09-11
 
        Destination-side conflicts are preserved. A legacy directory is removed only after
        all movable content has left it. Profile JSON is replaced atomically when changed.
    #>

    [CmdletBinding()]
    param (
        [System.String]
        $ProfilePath
    )

    $FunctionName = $MyInvocation.MyCommand.Name
    $moveDirectoryContent = {
        param (
            [System.String]$SourcePath,
            [System.String]$DestinationPath
        )

        if (-not (Test-Path -LiteralPath $SourcePath -PathType Container)) { return }

        if (-not (Test-Path -LiteralPath $DestinationPath -PathType Container)) {
            New-Item -ItemType Directory -Path $DestinationPath -Force | Out-Null
        }

        foreach ($Item in @(Get-ChildItem -LiteralPath $SourcePath -Force -ErrorAction SilentlyContinue)) {
            $DestinationItem = Join-Path $DestinationPath $Item.Name
            if (Test-Path -LiteralPath $DestinationItem) {
                if ($Item.PSIsContainer -and (Test-Path -LiteralPath $DestinationItem -PathType Container)) {
                    & $moveDirectoryContent $Item.FullName $DestinationItem
                    continue
                }

                Write-Warning "[$FunctionName] Could not migrate '$($Item.FullName)' because '$DestinationItem' already exists."
                continue
            }

            try {
                Move-Item -LiteralPath $Item.FullName -Destination $DestinationPath -ErrorAction Stop
                Write-Verbose "[$FunctionName] Migrated '$($Item.FullName)' to '$DestinationPath'"
            }
            catch {
                Write-Warning "[$FunctionName] Could not migrate '$($Item.FullName)' to '$DestinationPath': $($_.Exception.Message)"
            }
        }

        if (-not (Get-ChildItem -LiteralPath $SourcePath -Force -ErrorAction SilentlyContinue | Select-Object -First 1)) {
            Remove-Item -LiteralPath $SourcePath -Force -ErrorAction SilentlyContinue
        }
    }

    $SharedFolderMap = [ordered]@{
        'media-script'          = 'boot-mediascript'
        'build-mediascript'     = 'boot-mediascript'
        'winpe-script'          = 'boot-winpescript'
        'build-winpescript'     = 'boot-winpescript'
        'winpe-wallpaper'       = 'boot-wallpaper'
        'build-winpewallpaper'  = 'boot-wallpaper'
    }

    foreach ($LegacyFolderName in $SharedFolderMap.Keys) {
        & $moveDirectoryContent `
            (Join-Path $Script:OSDeployBootAssetsPath $LegacyFolderName) `
            (Join-Path $Script:OSDeployBootAssetsPath $SharedFolderMap[$LegacyFolderName])
    }

    if ([System.String]::IsNullOrWhiteSpace($ProfilePath) -or
        -not (Test-Path -LiteralPath $ProfilePath -PathType Container)) {
        return
    }

    $ProfileFolderMap = [ordered]@{
        'build-mediascript' = 'boot-mediascript'
        'build-winpescript' = 'boot-winpescript'
    }
    foreach ($LegacyFolderName in $ProfileFolderMap.Keys) {
        & $moveDirectoryContent `
            (Join-Path $ProfilePath $LegacyFolderName) `
            (Join-Path $ProfilePath $ProfileFolderMap[$LegacyFolderName])
    }

    $ProfileFile = Join-Path $ProfilePath 'osdeployboot.json'
    if (-not (Test-Path -LiteralPath $ProfileFile -PathType Leaf)) {
        return
    }

    try {
        $ProfileContent = Get-Content -LiteralPath $ProfileFile -Raw -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop
        $ProfileChanged = $false
        $PropertyFolderMap = [ordered]@{
            WinPEScript = [ordered]@{
                'winpe-script'      = 'boot-winpescript'
                'build-winpescript' = 'boot-winpescript'
            }
            MediaScript = [ordered]@{
                'media-script'      = 'boot-mediascript'
                'build-mediascript' = 'boot-mediascript'
            }
        }

        foreach ($PropertyName in $PropertyFolderMap.Keys) {
            $Property = $ProfileContent.PSObject.Properties[$PropertyName]
            if (-not $Property -or $null -eq $Property.Value) { continue }

            $IsArray = $Property.Value -is [System.Array]
            $UpdatedValues = @(
                foreach ($Entry in @($Property.Value)) {
                    $UpdatedEntry = $Entry
                    if ($Entry -is [System.String]) {
                        foreach ($LegacyFolderName in $PropertyFolderMap[$PropertyName].Keys) {
                            $EscapedFolderName = [System.Text.RegularExpressions.Regex]::Escape($LegacyFolderName)
                            $Pattern = "(^|[\\/])$EscapedFolderName(?=([\\/]|$))"
                            $UpdatedEntry = $UpdatedEntry -ireplace $Pattern, "`${1}$($PropertyFolderMap[$PropertyName][$LegacyFolderName])"
                        }
                        if ($UpdatedEntry -cne $Entry) { $ProfileChanged = $true }
                    }
                    $UpdatedEntry
                }
            )
            $Property.Value = if ($IsArray) { [System.Object[]]$UpdatedValues } else { $UpdatedValues[0] }
        }

        if ($ProfileChanged) {
            $TemporaryPath = Join-Path $ProfilePath ".$([System.Guid]::NewGuid()).tmp"
            try {
                $ProfileContent | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue |
                    Out-File -LiteralPath $TemporaryPath -Encoding utf8 -ErrorAction Stop
                [System.IO.File]::Move($TemporaryPath, $ProfileFile, $true)
            }
            finally {
                if (Test-Path -LiteralPath $TemporaryPath) {
                    Remove-Item -LiteralPath $TemporaryPath -Force -ErrorAction SilentlyContinue
                }
            }
        }
    }
    catch {
        Write-Warning "[$FunctionName] Could not update build profile '$ProfileFile': $($_.Exception.Message)"
    }
}