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 writable mirrors. A package is materialized
        when its complete file set is durable in at least one readable depot. Incomplete
        secondary mirrors remain visible in the result without discarding that success. A
        failed package is reported and the next package is attempted unless FailFast is selected.
        By default, the command never prompts for new catalog trust, imports signing keys,
        accepts unsigned definitions, installs packages, changes depot configuration, or removes
        files. AcceptUnknownSigningKey is an explicit opt-in for signed root definitions whose signing
        key is not yet trusted; unsigned and blocked root definitions remain excluded, while dependency trust follows the existing package policy.
 
    .PARAMETER AllTrusted
        Legacy compatibility switch. The already-trusted current-platform catalog is now the default scope.
 
    .PARAMETER AcceptUnknownSigningKey
        Also includes signed root definitions whose signing key is not yet trusted and explicitly accepts that key
        through the existing package trust workflow. Unsigned and blocked root definitions remain excluded; dependency trust follows the existing package policy.
 
    .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 = 'Medium')]
    param(
        [Parameter(DontShow = $true)]
        [switch]$AllTrusted,

        [switch]$AcceptUnknownSigningKey,

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

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

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

        [switch]$FailFast
    )

    if ($AllTrusted.IsPresent) {
        Write-Verbose '-AllTrusted is retained for compatibility; trusted current-platform definitions are now selected by default.'
    }

    $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
        }
    }

    $acceptedTrustStatuses = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
    $acceptedTrustStatuses.Add('signedTrusted') | Out-Null
    if ($AcceptUnknownSigningKey.IsPresent) {
        $acceptedTrustStatuses.Add('signedUnknownKeyPrompt') | Out-Null
        $acceptedTrustStatuses.Add('signedUnknownKeyAutoTrust') | Out-Null
    }

    $trustedCandidates = @(
        Search-Package @searchParameters |
            Where-Object {
                $acceptedTrustStatuses.Contains([string]$_.CatalogTrustStatus) -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 package definitions are available for the requested current-platform trust scope.'
        return
    }

    $trustParameters = if ($AcceptUnknownSigningKey.IsPresent) {
        @{ AcceptUnknownSigningKey = $true }
    }
    else {
        @{ RequireAlreadyTrusted = $true }
    }

    $targetScope = if ($AcceptUnknownSigningKey.IsPresent) {
        'trusted or explicitly accepted unknown-key current-platform package definition(s)'
    }
    else {
        'already trusted current-platform package definition(s)'
    }
    $target = '{0} {1}' -f $selected.Count, $targetScope
    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 @trustParameters
            [pscustomobject]@{
                PSTypeName     = 'Eigenverft.Manifested.Package.DepotMaterializeResult'
                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 @trustParameters)
        }
        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'
        }

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

        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."
    )
}