Commands/Depot/Eigenverft.Manifested.Package.Cmd.PackageDepot.ps1

<#
    Public depot-management command surface.
#>


function Get-PackageDepot {
    [CmdletBinding()]
    param(
        [string]$DepotId
    )

    $depots = @(Get-PackageDepotSummaries)
    if ([string]::IsNullOrWhiteSpace($DepotId)) {
        return $depots
    }

    $match = @($depots | Where-Object { [string]::Equals([string]$_.DepotId, $DepotId, [System.StringComparison]::OrdinalIgnoreCase) })
    if ($match.Count -eq 0) {
        throw "Package depot '$DepotId' was not found in '$((Get-PackageDepotInventoryPath))'."
    }

    return $match
}

function Invoke-PackageDepotMaterialize {
    <#
    .SYNOPSIS
        Materializes every already trusted package available for the current platform.
 
    .DESCRIPTION
        Discovers package definitions whose effective catalog status is signedTrusted,
        deduplicates their publisher/definition identities, and materializes each package
        plus its dependencies into configured depots. The command never prompts for new
        catalog trust, imports signing keys, accepts unsigned definitions, or removes files.
 
        Sync-PackageDepot is a deprecated alias for this command.
 
    .PARAMETER AllTrusted
        Confirms that the complete already-trusted current-platform catalog is the materialize scope.
 
    .PARAMETER ExcludeDefinitionId
        Omits matching definition ids from this run.
 
    .PARAMETER FailFast
        Stops after the first package that does not materialize successfully.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    [Alias('Sync-PackageDepot')]
    param(
        [Parameter(Mandatory = $true)]
        [switch]$AllTrusted,

        [AllowNull()]
        [string]$PublisherId = $null,

        [AllowNull()]
        [string[]]$Tag = @(),

        [AllowNull()]
        [string[]]$ExcludeDefinitionId = @(),

        [switch]$FailFast
    )

    if ([string]::Equals($MyInvocation.InvocationName, 'Sync-PackageDepot', [System.StringComparison]::OrdinalIgnoreCase)) {
        Write-Warning "Sync-PackageDepot is deprecated. Use Invoke-PackageDepotMaterialize instead."
    }

    if (-not $AllTrusted.IsPresent) {
        throw 'Invoke-PackageDepotMaterialize currently requires -AllTrusted.'
    }

    $searchParameters = @{
        CurrentPlatformOnly = $true
        IncludeIneligible   = $true
    }
    if (-not [string]::IsNullOrWhiteSpace($PublisherId)) {
        $searchParameters.PublisherId = $PublisherId
    }
    $normalizedTags = @($Tag | ForEach-Object { ([string]$_).Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
    if ($normalizedTags.Count -gt 0) {
        $searchParameters.Tag = $normalizedTags
    }

    $excluded = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
    foreach ($definitionId in @($ExcludeDefinitionId)) {
        $normalizedDefinitionId = ([string]$definitionId).Trim()
        if (-not [string]::IsNullOrWhiteSpace($normalizedDefinitionId)) {
            $excluded.Add($normalizedDefinitionId) | Out-Null
        }
    }

    $trustedCandidates = @(
        Search-Package @searchParameters |
            Where-Object {
                [string]::Equals([string]$_.CatalogTrustStatus, 'signedTrusted', [System.StringComparison]::OrdinalIgnoreCase) -and
                -not $excluded.Contains([string]$_.DefinitionId)
            }
    )

    $selected = @(
        $trustedCandidates |
            Sort-Object -Property PublisherId, DefinitionId, EndpointSearchOrder, @{ Expression = 'DefinitionRevision'; Descending = $true } |
            Group-Object -Property { Get-PackageDependencyReferenceKey -PublisherId ([string]$_.PublisherId) -DefinitionId ([string]$_.DefinitionId) } |
            ForEach-Object { $_.Group | Select-Object -First 1 } |
            Sort-Object -Property PublisherId, DefinitionId
    )

    if ($selected.Count -eq 0) {
        Write-Verbose 'No already signed-and-trusted package definitions are available for the requested current-platform scope.'
        return
    }

    $target = '{0} already trusted current-platform package definition(s)' -f $selected.Count
    if (-not $PSCmdlet.ShouldProcess($target, 'Materialize complete artifact sets into configured package depots')) {
        foreach ($package in $selected) {
            $assignmentPlan = New-PackageAssignmentPlanCore -PublisherId ([string]$package.PublisherId) -DefinitionId ([string]$package.DefinitionId) -Purpose Inspection -MaterializeOnly -RequireAlreadyTrusted
            [pscustomobject]@{
                PSTypeName    = 'Eigenverft.Manifested.Package.DepotSyncResult'
                PublisherId   = [string]$package.PublisherId
                DefinitionId  = [string]$package.DefinitionId
                Version       = [string]$package.Version
                Status        = if ($assignmentPlan.Accepted) { 'Planned' } else { 'Blocked' }
                AssignmentPlan = $assignmentPlan
                BlockerSummary = @($assignmentPlan.Blockers)
                PackageResult = @()
                ErrorMessage  = $null
            }
        }
        return
    }

    foreach ($package in $selected) {
        $packageResults = @()
        $errorMessage = $null
        try {
            $packageResults = @(Invoke-Package -PublisherId ([string]$package.PublisherId) -DefinitionId ([string]$package.DefinitionId) -MaterializeOnly -RequireAlreadyTrusted)
        }
        catch {
            $errorMessage = $_.Exception.Message
        }

        $failedResults = @($packageResults | Where-Object {
                -not ([string]$_.Status -in @('Ready', 'Materialized'))
            })
        $status = if (-not [string]::IsNullOrWhiteSpace($errorMessage) -or $failedResults.Count -gt 0 -or $packageResults.Count -eq 0) {
            'Failed'
        }
        else {
            'Materialized'
        }

        $syncResult = [pscustomobject]@{
            PSTypeName    = 'Eigenverft.Manifested.Package.DepotSyncResult'
            PublisherId   = [string]$package.PublisherId
            DefinitionId  = [string]$package.DefinitionId
            Version       = [string]$package.Version
            Status        = $status
            AssignmentPlan = $null
            BlockerSummary = @()
            PackageResult = @($packageResults)
            ErrorMessage  = $errorMessage
        }
        $syncResult

        if ($FailFast.IsPresent -and [string]::Equals($status, 'Failed', [System.StringComparison]::OrdinalIgnoreCase)) {
            break
        }
    }
}

function Add-PackageDepot {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$DepotId,

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

        [int]$SearchOrder = -1,

        [string]$After,

        [string[]]$SiteCode = @(),

        [switch]$Disabled,

        [switch]$Writable,

        [switch]$MirrorTarget,

        [switch]$EnsureExists
    )

    Assert-PackageDepotId -DepotId $DepotId

    $documentInfo = Get-PackageDepotInventoryEditInfo
    if (Get-PackageDepotSourceProperty -Document $documentInfo.Document -DepotId $DepotId) {
        throw "Package depot '$DepotId' already exists in '$($documentInfo.Path)'. Use Set-PackageDepot to modify it."
    }

    $resolvedSearchOrder = if ($SearchOrder -ge 0) {
        $SearchOrder
    }
    elseif (-not [string]::IsNullOrWhiteSpace($After)) {
        Get-PackageDepotSearchOrderAfter -Document $documentInfo.Document -AfterDepotId $After
    }
    else {
        Get-PackageNextDepotSearchOrder -Document $documentInfo.Document
    }
    $source = New-PackageFilesystemDepotSource -BasePath $BasePath -SearchOrder $resolvedSearchOrder -Enabled (-not $Disabled.IsPresent) -SiteCodes $SiteCode -Writable $Writable.IsPresent -MirrorTarget $MirrorTarget.IsPresent -EnsureExists $EnsureExists.IsPresent

    if ($PSCmdlet.ShouldProcess($documentInfo.Path, "Add package depot '$DepotId'")) {
        $documentInfo.Document.acquisitionEnvironment.environmentSources | Add-Member -MemberType NoteProperty -Name $DepotId -Value $source -Force
        Save-PackageDepotInventoryDocument -DocumentInfo $documentInfo
    }

    $after = Get-PackageDepot -DepotId $DepotId
    $notes = New-Object System.Collections.Generic.List[string]
    if ($Disabled.IsPresent) {
        $notes.Add("Depot '$DepotId' was added disabled; package acquisition will not use it until enabled.") | Out-Null
    }
    if (-not $Writable.IsPresent) {
        $notes.Add("Depot '$DepotId' is read-only from Package perspective by default; downloads will not be mirrored there.") | Out-Null
    }

    return New-PackageDepotCommandResult -Action 'Add' -DepotId $DepotId -InventoryPath $documentInfo.Path -Before $null -After $after -Status 'Added' -Notes @($notes.ToArray())
}

function Add-TeamPackageDepot {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$BasePath,

        [ValidateNotNullOrEmpty()]
        [string]$DepotId = 'teamPackageDepot',

        [int]$SearchOrder = -1,

        [string]$After,

        [string[]]$SiteCode = @(),

        [switch]$Disabled
    )

    $parameters = @{
        DepotId      = $DepotId
        BasePath     = $BasePath
        SiteCode     = $SiteCode
        Writable     = $true
        MirrorTarget = $true
        EnsureExists = $true
    }
    if ($SearchOrder -ge 0) {
        $parameters.SearchOrder = $SearchOrder
    }
    elseif (-not [string]::IsNullOrWhiteSpace($After)) {
        $parameters.After = $After
    }
    else {
        $parameters.SearchOrder = 150
    }
    if ($Disabled.IsPresent) {
        $parameters.Disabled = $true
    }
    if ($PSBoundParameters.ContainsKey('WhatIf')) {
        $parameters.WhatIf = [bool]$PSBoundParameters.WhatIf
    }
    if ($PSBoundParameters.ContainsKey('Confirm')) {
        $parameters.Confirm = [bool]$PSBoundParameters.Confirm
    }

    return Add-PackageDepot @parameters
}

function Set-PackageDepot {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$DepotId,

        [string]$BasePath,

        [Nullable[int]]$SearchOrder,

        [string[]]$SiteCode,

        [switch]$ClearSiteCode,

        [switch]$Enable,

        [switch]$Disable,

        [switch]$Readable,

        [switch]$NotReadable,

        [switch]$Writable,

        [switch]$ReadOnly,

        [switch]$MirrorTarget,

        [switch]$NoMirrorTarget,

        [switch]$EnsureExists,

        [switch]$NoEnsureExists
    )

    Assert-PackageDepotId -DepotId $DepotId
    if ($Enable.IsPresent -and $Disable.IsPresent) { throw 'Use either -Enable or -Disable, not both.' }
    if ($Readable.IsPresent -and $NotReadable.IsPresent) { throw 'Use either -Readable or -NotReadable, not both.' }
    if ($Writable.IsPresent -and $ReadOnly.IsPresent) { throw 'Use either -Writable or -ReadOnly, not both.' }
    if ($MirrorTarget.IsPresent -and $NoMirrorTarget.IsPresent) { throw 'Use either -MirrorTarget or -NoMirrorTarget, not both.' }
    if ($EnsureExists.IsPresent -and $NoEnsureExists.IsPresent) { throw 'Use either -EnsureExists or -NoEnsureExists, not both.' }
    if ($ClearSiteCode.IsPresent -and $PSBoundParameters.ContainsKey('SiteCode')) { throw 'Use either -SiteCode or -ClearSiteCode, not both.' }

    $before = Get-PackageDepot -DepotId $DepotId
    $documentInfo = Get-PackageDepotInventoryEditInfo
    $sourceProperty = Get-PackageDepotSourceProperty -Document $documentInfo.Document -DepotId $DepotId
    if (-not $sourceProperty) {
        throw "Package depot '$DepotId' was not found in '$($documentInfo.Path)'. Use Add-PackageDepot to create it."
    }

    $source = $sourceProperty.Value
    if (-not [string]::Equals([string]$source.kind, 'filesystem', [System.StringComparison]::OrdinalIgnoreCase)) {
        throw "Package depot '$DepotId' is kind '$($source.kind)'. Only filesystem depots can be modified by Set-PackageDepot."
    }

    if ($PSBoundParameters.ContainsKey('BasePath')) { $source | Add-Member -MemberType NoteProperty -Name 'basePath' -Value $BasePath -Force }
    if ($PSBoundParameters.ContainsKey('SearchOrder')) { $source | Add-Member -MemberType NoteProperty -Name 'searchOrder' -Value ([int]$SearchOrder.Value) -Force }
    if ($Enable.IsPresent) { $source | Add-Member -MemberType NoteProperty -Name 'enabled' -Value $true -Force }
    if ($Disable.IsPresent) { $source | Add-Member -MemberType NoteProperty -Name 'enabled' -Value $false -Force }
    if ($Readable.IsPresent) { $source | Add-Member -MemberType NoteProperty -Name 'readable' -Value $true -Force }
    if ($NotReadable.IsPresent) { $source | Add-Member -MemberType NoteProperty -Name 'readable' -Value $false -Force }
    if ($Writable.IsPresent) { $source | Add-Member -MemberType NoteProperty -Name 'writable' -Value $true -Force }
    if ($ReadOnly.IsPresent) {
        $source | Add-Member -MemberType NoteProperty -Name 'writable' -Value $false -Force
        $source | Add-Member -MemberType NoteProperty -Name 'mirrorTarget' -Value $false -Force
        $source | Add-Member -MemberType NoteProperty -Name 'ensureExists' -Value $false -Force
    }
    if ($MirrorTarget.IsPresent) { $source | Add-Member -MemberType NoteProperty -Name 'mirrorTarget' -Value $true -Force }
    if ($NoMirrorTarget.IsPresent) { $source | Add-Member -MemberType NoteProperty -Name 'mirrorTarget' -Value $false -Force }
    if ($EnsureExists.IsPresent) { $source | Add-Member -MemberType NoteProperty -Name 'ensureExists' -Value $true -Force }
    if ($NoEnsureExists.IsPresent) { $source | Add-Member -MemberType NoteProperty -Name 'ensureExists' -Value $false -Force }
    if ($PSBoundParameters.ContainsKey('SiteCode')) { $source | Add-Member -MemberType NoteProperty -Name 'siteCodes' -Value @($SiteCode | ForEach-Object { ([string]$_).Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -Force }
    if ($ClearSiteCode.IsPresent -and $source.PSObject.Properties['siteCodes']) { $source.PSObject.Properties.Remove('siteCodes') }

    if ([bool]$source.mirrorTarget -and -not [bool]$source.writable) {
        throw "Package depot '$DepotId' cannot use MirrorTarget=true while Writable=false. Add -Writable or remove -MirrorTarget."
    }
    if ([bool]$source.ensureExists -and -not [bool]$source.writable) {
        throw "Package depot '$DepotId' cannot use EnsureExists=true while Writable=false. Add -Writable or remove -EnsureExists."
    }

    if ($PSCmdlet.ShouldProcess($documentInfo.Path, "Set package depot '$DepotId'")) {
        Save-PackageDepotInventoryDocument -DocumentInfo $documentInfo
    }

    $after = Get-PackageDepot -DepotId $DepotId
    $notes = New-Object System.Collections.Generic.List[string]
    if (-not [bool]$after.Enabled) {
        $notes.Add("Depot '$DepotId' was updated but remains disabled; package acquisition will not use it until enabled.") | Out-Null
    }
    elseif (-not [bool]$after.Effective) {
        $notes.Add("Depot '$DepotId' is enabled but is not currently effective. Check site codes or config validation notes.") | Out-Null
    }
    if ($PSBoundParameters.ContainsKey('BasePath')) {
        $notes.Add("Depot '$DepotId' base path is now '$($after.BasePath)' and resolves to '$($after.ResolvedBasePath)'.") | Out-Null
    }

    return New-PackageDepotCommandResult -Action 'Set' -DepotId $DepotId -InventoryPath $documentInfo.Path -Before $before -After $after -Status 'Updated' -Notes @($notes.ToArray())
}

function Remove-PackageDepot {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$DepotId,

        [switch]$Force
    )

    Assert-PackageDepotId -DepotId $DepotId
    if ([string]::Equals($DepotId, 'defaultPackageDepot', [System.StringComparison]::OrdinalIgnoreCase) -and -not $Force.IsPresent) {
        throw "Removing 'defaultPackageDepot' can leave Package without a local durable depot. Re-run with -Force if this is intentional."
    }

    $before = Get-PackageDepot -DepotId $DepotId
    $documentInfo = Get-PackageDepotInventoryEditInfo
    $sourceProperty = Get-PackageDepotSourceProperty -Document $documentInfo.Document -DepotId $DepotId
    if (-not $sourceProperty) {
        throw "Package depot '$DepotId' was not found in '$($documentInfo.Path)'."
    }

    if ($PSCmdlet.ShouldProcess($documentInfo.Path, "Remove package depot '$DepotId'")) {
        $documentInfo.Document.acquisitionEnvironment.environmentSources.PSObject.Properties.Remove($DepotId)
        Save-PackageDepotInventoryDocument -DocumentInfo $documentInfo
    }

    return New-PackageDepotCommandResult -Action 'Remove' -DepotId $DepotId -InventoryPath $documentInfo.Path -Before $before -After $null -Status 'Removed' -Notes @(
        "Depot '$DepotId' was removed from configuration only. Existing depot files were not deleted."
    )
}