Support/Package/Execution/Eigenverft.Manifested.Package.Package.PathRegistration.ps1

<#
    Eigenverft.Manifested.Package.Package.PathRegistration
#>


function Add-PackagePathCleanupDirectory {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyCollection()]
        [System.Collections.Generic.List[string]]$CleanupDirectories,

        [AllowNull()]
        [string]$DirectoryPath
    )

    $normalizedDirectoryPath = Get-NormalizedPathEntry -PathEntry $DirectoryPath
    if ([string]::IsNullOrWhiteSpace($normalizedDirectoryPath)) {
        return
    }

    if ($normalizedDirectoryPath -notin @($CleanupDirectories.ToArray())) {
        $CleanupDirectories.Add($normalizedDirectoryPath) | Out-Null
    }
}

function Get-PackagePathRegistrationSourceValues {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [psobject]$PackageResult,

        [AllowNull()]
        [psobject]$Source
    )

    $values = New-Object System.Collections.Generic.List[string]
    $sourceKind = if ($Source -and $Source.PSObject.Properties['kind']) { [string]$Source.kind } else { $null }
    if ([string]::Equals($sourceKind, 'shim', [System.StringComparison]::OrdinalIgnoreCase)) {
        if (-not $Source.PSObject.Properties['use'] -or
            -not [string]::Equals([string]$Source.use, 'discovery.presence.commands', [System.StringComparison]::Ordinal)) {
            throw "Package pathRegistration source kind 'shim' requires source.use = 'discovery.presence.commands'."
        }
        foreach ($command in @(Get-PackageDiscoveryPresenceEntryPoints -Definition $PackageResult.PackageConfig.Definition -ToolKind 'commands' -ExposedOnly)) {
            $commandName = ([string]$command.name).Trim()
            if (-not [string]::IsNullOrWhiteSpace($commandName) -and $commandName -notin @($values.ToArray())) {
                $values.Add($commandName) | Out-Null
            }
        }
        return @($values.ToArray())
    }

    if ($Source -and $Source.PSObject.Properties['value'] -and
        -not [string]::IsNullOrWhiteSpace([string]$Source.value)) {
        $values.Add([string]$Source.value) | Out-Null
    }

    if ($Source -and $Source.PSObject.Properties['values'] -and $null -ne $Source.values) {
        foreach ($value in @($Source.values)) {
            $textValue = ([string]$value).Trim()
            if ([string]::IsNullOrWhiteSpace($textValue)) {
                continue
            }
            if ($textValue -notin @($values.ToArray())) {
                $values.Add($textValue) | Out-Null
            }
        }
    }

    return @($values.ToArray())
}

function Get-PackagePathRegistrationSourcePath {
<#
.SYNOPSIS
Resolves the raw source path for PATH registration.
 
.DESCRIPTION
Interprets the configured pathRegistration source and returns the concrete file
or directory path for the requested install directory.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult,

        [string]$InstallDirectoryOverride
    )

    $assigned = Get-PackageAssignedOperation -Release $PackageResult.Package
    if (-not $assigned -or -not $assigned.PSObject.Properties['pathRegistration'] -or $null -eq $assigned.pathRegistration) {
        return $null
    }

    $pathRegistration = $assigned.pathRegistration
    if (-not $pathRegistration.PSObject.Properties['source'] -or $null -eq $pathRegistration.source) {
        return $null
    }

    $source = $pathRegistration.source
    $sourceKind = if ($source.PSObject.Properties['kind']) { [string]$source.kind } else { $null }
    $sourceValues = @(Get-PackagePathRegistrationSourceValues -PackageResult $PackageResult -Source $source)
    $sourceValue = if ($sourceValues.Count -gt 0) { [string]$sourceValues[0] } else { $null }
    if ([string]::IsNullOrWhiteSpace($sourceKind) -or $sourceValues.Count -eq 0) {
        throw "Package pathRegistration requires source.kind and a resolvable source value when pathRegistration.mode is not 'none'."
    }

    $baseInstallDirectory = if (-not [string]::IsNullOrWhiteSpace($InstallDirectoryOverride)) {
        $InstallDirectoryOverride
    }
    else {
        $PackageResult.InstallDirectory
    }

    switch -Exact ($sourceKind) {
        'commandEntryPoint' {
            if ($sourceValues.Count -gt 1) {
                throw "Package pathRegistration source kind 'commandEntryPoint' supports only one source value."
            }
            $sourcePath = Resolve-PackagePresenceToolPath -Definition $PackageResult.PackageConfig.Definition -ToolKind 'commands' -Name $sourceValue -InstallDirectory $baseInstallDirectory
            if (-not [string]::IsNullOrWhiteSpace($sourcePath)) {
                return $sourcePath
            }
            throw "Package pathRegistration source commandEntryPoint '$sourceValue' was not found in discovery.presence.commands."
        }
        'appEntryPoint' {
            if ($sourceValues.Count -gt 1) {
                throw "Package pathRegistration source kind 'appEntryPoint' supports only one source value."
            }
            $sourcePath = Resolve-PackagePresenceToolPath -Definition $PackageResult.PackageConfig.Definition -ToolKind 'apps' -Name $sourceValue -InstallDirectory $baseInstallDirectory
            if (-not [string]::IsNullOrWhiteSpace($sourcePath)) {
                return $sourcePath
            }
            throw "Package pathRegistration source appEntryPoint '$sourceValue' was not found in discovery.presence.apps."
        }
        'installRelativeDirectory' {
            if ($sourceValues.Count -gt 1) {
                throw "Package pathRegistration source kind 'installRelativeDirectory' supports only one source value."
            }
            return (Join-Path $baseInstallDirectory (($sourceValue) -replace '/', '\'))
        }
        'shim' {
            if (-not [string]::IsNullOrWhiteSpace($InstallDirectoryOverride)) {
                return (Get-PackageCommandShimPath -PackageResult $PackageResult -CommandName $sourceValues[0])
            }

            $shimResults = @(
                foreach ($commandName in $sourceValues) {
                    New-PackageCommandShim -PackageResult $PackageResult -CommandName $commandName
                }
            )
            return $shimResults[0].ShimPath
        }
        default {
            throw "Unsupported Package pathRegistration source kind '$sourceKind'."
        }
    }
}

function Test-PackageShouldApplyPathRegistration {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult
    )

    return ([string]$PackageResult.InstallOrigin) -in @('PackageInstalled', 'PackageReused')
}

function Get-PackagePathRegistrationCleanupDirectories {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult
    )

    $currentInstallDirectory = Get-NormalizedPathEntry -PathEntry ([string]$PackageResult.InstallDirectory)
    $cleanupDirectories = New-Object System.Collections.Generic.List[string]
    $candidateInstallDirectories = New-Object System.Collections.Generic.List[string]
    $assignedForPath = Get-PackageAssignedOperation -Release $PackageResult.Package
    if (-not $assignedForPath -or -not $assignedForPath.PSObject.Properties['pathRegistration'] -or $null -eq $assignedForPath.pathRegistration) {
        return @()
    }
    $currentSource = $assignedForPath.pathRegistration.source
    $currentSourceKind = if ($currentSource.PSObject.Properties['kind']) {
        [string]$assignedForPath.pathRegistration.source.kind
    }
    else {
        $null
    }
    $currentSourceValues = @(Get-PackagePathRegistrationSourceValues -PackageResult $PackageResult -Source $currentSource)

    if ($PackageResult.ExistingPackage -and
        [string]::Equals([string]$PackageResult.ExistingPackage.Classification, 'PackageTarget', [System.StringComparison]::OrdinalIgnoreCase) -and
        -not [string]::IsNullOrWhiteSpace([string]$PackageResult.ExistingPackage.InstallDirectory)) {
        $candidateInstallDirectories.Add([string]$PackageResult.ExistingPackage.InstallDirectory) | Out-Null
    }

    if ($PackageResult.Ownership -and
        $PackageResult.Ownership.OwnershipRecord -and
        $PackageResult.Ownership.OwnershipRecord.PSObject.Properties['installDirectory'] -and
        -not [string]::IsNullOrWhiteSpace([string]$PackageResult.Ownership.OwnershipRecord.installDirectory)) {
        $candidateInstallDirectories.Add([string]$PackageResult.Ownership.OwnershipRecord.installDirectory) | Out-Null
    }

    foreach ($candidateInstallDirectory in @($candidateInstallDirectories.ToArray())) {
        $normalizedCandidateInstallDirectory = Get-NormalizedPathEntry -PathEntry $candidateInstallDirectory
        if ([string]::IsNullOrWhiteSpace($normalizedCandidateInstallDirectory) -or
            [string]::Equals($normalizedCandidateInstallDirectory, $currentInstallDirectory, [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        if ([string]::Equals($currentSourceKind, 'shim', [System.StringComparison]::OrdinalIgnoreCase)) {
            continue
        }

        $candidateSourcePath = Get-PackagePathRegistrationSourcePath -PackageResult $PackageResult -InstallDirectoryOverride $candidateInstallDirectory
        $candidateSourceKind = $currentSourceKind
        $candidateRegisteredDirectory = Resolve-PathRegistrationDirectory -SourcePath $candidateSourcePath -SourceKind $candidateSourceKind
        Add-PackagePathCleanupDirectory -CleanupDirectories $cleanupDirectories -DirectoryPath $candidateRegisteredDirectory
    }

    if ([string]::Equals($currentSourceKind, 'shim', [System.StringComparison]::OrdinalIgnoreCase) -and
        $currentSourceValues.Count -gt 0) {
        $directCleanupInstallDirectories = New-Object System.Collections.Generic.List[string]
        $directCandidateInstallDirectories = @($candidateInstallDirectories.ToArray()) + @([string]$PackageResult.InstallDirectory)
        foreach ($candidateInstallDirectory in $directCandidateInstallDirectories) {
            if ([string]::IsNullOrWhiteSpace($candidateInstallDirectory)) {
                continue
            }
            $normalizedCandidateInstallDirectory = Get-NormalizedPathEntry -PathEntry $candidateInstallDirectory
            if ([string]::IsNullOrWhiteSpace($normalizedCandidateInstallDirectory) -or
                $normalizedCandidateInstallDirectory -in @($directCleanupInstallDirectories.ToArray())) {
                continue
            }
            $directCleanupInstallDirectories.Add($normalizedCandidateInstallDirectory) | Out-Null
        }

        foreach ($candidateInstallDirectory in @($directCleanupInstallDirectories.ToArray())) {
            foreach ($currentSourceValue in $currentSourceValues) {
                $directCommandPath = Resolve-PackagePresenceToolPath -Definition $PackageResult.PackageConfig.Definition -ToolKind 'commands' -Name $currentSourceValue -InstallDirectory $candidateInstallDirectory
                if ([string]::IsNullOrWhiteSpace($directCommandPath)) {
                    continue
                }
                $directCommandDirectory = Resolve-PathRegistrationDirectory -SourcePath $directCommandPath -SourceKind 'commandEntryPoint'
                Add-PackagePathCleanupDirectory -CleanupDirectories $cleanupDirectories -DirectoryPath $directCommandDirectory
            }
        }
    }

    return @($cleanupDirectories.ToArray())
}

function Register-PackagePath {
<#
.SYNOPSIS
Applies Package PATH registration for a validated install.
 
.DESCRIPTION
Updates process and persisted PATH scopes according to assigned.pathRegistration.
User mode updates Process and User PATH. Machine mode updates Process and
Machine PATH. Writing User/Machine Path always resynchronizes Process Path via
Sync-ProcessPathEnvironment. None skips registration. Package only writes PATH
entries for Package-owned outcomes and only cleans stale Package-owned paths
for the same install slot.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult
    )

    $assigned = Get-PackageAssignedOperation -Release $PackageResult.Package
    $pathRegistration = if ($assigned -and $assigned.PSObject.Properties['pathRegistration']) { $assigned.pathRegistration } else { $null }
    $mode = if ($pathRegistration -and $pathRegistration.PSObject.Properties['mode'] -and -not [string]::IsNullOrWhiteSpace([string]$pathRegistration.mode)) {
        ([string]$pathRegistration.mode).ToLowerInvariant()
    }
    else {
        'none'
    }

    $source = if ($pathRegistration -and $pathRegistration.PSObject.Properties['source']) { $pathRegistration.source } else { $null }
    $sourceKind = if ($source -and $source.PSObject.Properties['kind']) { [string]$source.kind } else { $null }
    $sourceValues = @(Get-PackagePathRegistrationSourceValues -PackageResult $PackageResult -Source $source)
    $sourceValue = if ($sourceValues.Count -gt 0) { $sourceValues -join ',' } else { $null }

    if ($mode -eq 'none') {
        $pathRegistrationResult = [pscustomobject]@{
            Status             = 'Skipped'
            Mode               = 'none'
            SourceKind         = $sourceKind
            SourceValue        = $sourceValue
            SourceValues       = @($sourceValues)
            SourcePath         = $null
            RegisteredPath     = $null
            CleanupDirectories = @()
            CleanedTargets     = @()
            UpdatedTargets     = @()
        }
        if ($PackageResult.PSObject.Properties['PathRegistration']) {
            $PackageResult.PathRegistration = $pathRegistrationResult
        }
        else {
            $PackageResult | Add-Member -MemberType NoteProperty -Name PathRegistration -Value $pathRegistrationResult
        }
        Write-PackageExecutionMessage -Message '[STATE] PATH registration skipped because mode is none.'
        return $PackageResult
    }

    if ($mode -notin @('user', 'machine')) {
        throw "Unsupported Package pathRegistration.mode '$mode'."
    }

    if (-not (Test-PackageShouldApplyPathRegistration -PackageResult $PackageResult)) {
        $pathRegistrationResult = [pscustomobject]@{
            Status             = 'SkippedNotPackageOwned'
            Mode               = $mode
            SourceKind         = $sourceKind
            SourceValue        = $sourceValue
            SourceValues       = @($sourceValues)
            SourcePath         = $null
            RegisteredPath     = $null
            CleanupDirectories = @()
            CleanedTargets     = @()
            UpdatedTargets     = @()
        }
        if ($PackageResult.PSObject.Properties['PathRegistration']) {
            $PackageResult.PathRegistration = $pathRegistrationResult
        }
        else {
            $PackageResult | Add-Member -MemberType NoteProperty -Name PathRegistration -Value $pathRegistrationResult
        }

        Write-PackageExecutionMessage -Message ("[STATE] PATH registration skipped because installOrigin '{0}' is not Package-owned." -f [string]$PackageResult.InstallOrigin)
        return $PackageResult
    }

    $sourcePath = Get-PackagePathRegistrationSourcePath -PackageResult $PackageResult
    if ([string]::IsNullOrWhiteSpace($sourcePath) -or -not (Test-Path -LiteralPath $sourcePath)) {
        throw "Package PATH registration source path '$sourcePath' was not found."
    }

    $registeredPath = Resolve-PathRegistrationDirectory -SourcePath $sourcePath -SourceKind $sourceKind
    $cleanupDirectories = @(Get-PackagePathRegistrationCleanupDirectories -PackageResult $PackageResult)
    $registrationResult = Register-PathEnvironment -Mode $mode -RegisteredPath $registeredPath -CleanupDirectories $cleanupDirectories

    $pathRegistrationResult = [pscustomobject]@{
        Status             = $registrationResult.Status
        Mode               = $registrationResult.Mode
        SourceKind         = $sourceKind
        SourceValue        = $sourceValue
        SourceValues       = @($sourceValues)
        SourcePath         = $sourcePath
        RegisteredPath     = $registrationResult.RegisteredPath
        CleanupDirectories = @($registrationResult.CleanupDirectories)
        CleanedTargets     = @($registrationResult.CleanedTargets)
        UpdatedTargets     = @($registrationResult.UpdatedTargets)
    }
    if ($PackageResult.PSObject.Properties['PathRegistration']) {
        $PackageResult.PathRegistration = $pathRegistrationResult
    }
    else {
        $PackageResult | Add-Member -MemberType NoteProperty -Name PathRegistration -Value $pathRegistrationResult
    }

    Write-PackageExecutionMessage -Message ("[STATE] PATH registration resolved source kind='{0}' value='{1}' to '{2}'." -f $sourceKind, $sourceValue, $registeredPath)
    if ($cleanupDirectories.Count -gt 0) {
        Write-PackageExecutionMessage -Message ("[STATE] PATH cleanup directories for this Package install slot: {0}" -f ($cleanupDirectories -join ', '))
    }
    Write-PackageExecutionMessage -Message ("[ACTION] PATH registration status='{0}' mode='{1}' updatedTargets='{2}' cleanedTargets='{3}'." -f $PackageResult.PathRegistration.Status, $mode, $(if ($registrationResult.UpdatedTargets.Count -gt 0) { @($registrationResult.UpdatedTargets) -join ',' } else { '<none>' }), $(if ($registrationResult.CleanedTargets.Count -gt 0) { @($registrationResult.CleanedTargets) -join ',' } else { '<none>' }))

    return $PackageResult
}

function Unregister-PackagePathForRemoval {
<#
.SYNOPSIS
Removes Package PATH entries recorded during assignment, without re-adding a primary path.
 
.DESCRIPTION
Uses the persisted pathRegistration snapshot from inventory (attached to
PackageResult.PathRegistration) to remove registeredPath and cleanup
directories from PATH for the same mode as registration.
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [psobject]$PackageResult
    )

    $pathRegistration = if ($PackageResult.PSObject.Properties['PathRegistration'] -and $null -ne $PackageResult.PathRegistration) {
        $PackageResult.PathRegistration
    }
    else {
        $null
    }

    if (-not $pathRegistration -or -not $pathRegistration.PSObject.Properties['mode']) {
        Write-PackageExecutionMessage -Message '[STATE] PATH unregistration skipped because no pathRegistration was resolved.'
        return $PackageResult
    }

    $mode = ([string]$pathRegistration.mode).ToLowerInvariant()
    if ($mode -eq 'none' -or [string]::Equals([string]$pathRegistration.Status, 'Skipped', [System.StringComparison]::OrdinalIgnoreCase) -or
        [string]::Equals([string]$pathRegistration.Status, 'SkippedNotPackageOwned', [System.StringComparison]::OrdinalIgnoreCase)) {
        Write-PackageExecutionMessage -Message '[STATE] PATH unregistration skipped because registration was not active.'
        return $PackageResult
    }

    if ($mode -notin @('user', 'machine')) {
        throw "Unsupported Package pathRegistration.mode '$mode' for removal."
    }

    $registeredPath = if ($pathRegistration.PSObject.Properties['registeredPath'] -and -not [string]::IsNullOrWhiteSpace([string]$pathRegistration.registeredPath)) {
        [System.IO.Path]::GetFullPath([string]$pathRegistration.registeredPath)
    }
    else {
        $null
    }

    $cleanupDirectories = New-Object System.Collections.Generic.List[string]
    if ($registeredPath) {
        $cleanupDirectories.Add($registeredPath) | Out-Null
    }

    $cleanupFromRecord = if ($pathRegistration.PSObject.Properties['CleanupDirectories'] -and $null -ne $pathRegistration.CleanupDirectories) {
        @($pathRegistration.CleanupDirectories)
    }
    else {
        @()
    }
    foreach ($dir in @($cleanupFromRecord)) {
        if (-not [string]::IsNullOrWhiteSpace([string]$dir)) {
            $normalized = Get-NormalizedPathEntry -PathEntry ([string]$dir)
            if ($normalized -and $normalized -notin @($cleanupDirectories.ToArray())) {
                $cleanupDirectories.Add($normalized) | Out-Null
            }
        }
    }

    if ($PackageResult.InstallDirectory -and (Test-Path -LiteralPath $PackageResult.InstallDirectory -PathType Container)) {
        foreach ($extra in @(Get-PackagePathRegistrationCleanupDirectories -PackageResult $PackageResult)) {
            $normalized = Get-NormalizedPathEntry -PathEntry $extra
            if ($normalized -and $normalized -notin @($cleanupDirectories.ToArray())) {
                $cleanupDirectories.Add($normalized) | Out-Null
            }
        }
    }

    if ($cleanupDirectories.Count -eq 0) {
        Write-PackageExecutionMessage -Message '[STATE] PATH unregistration skipped because no directories were recorded for cleanup.'
        return $PackageResult
    }

    $unregisterResult = Remove-PathEnvironmentDirectories -Mode $mode -DirectoryPaths @($cleanupDirectories.ToArray())
    Write-PackageExecutionMessage -Message ("[ACTION] PATH unregistration status='{0}' mode='{1}' updatedTargets='{2}'." -f $unregisterResult.Status, $mode, $(if ($unregisterResult.UpdatedTargets.Count -gt 0) { @($unregisterResult.UpdatedTargets) -join ',' } else { '<none>' }))

    return $PackageResult
}