Public/func_Install-Package.ps1

Function Install-Package {
    <#
    .SYNOPSIS
    Installs CDF template and config packages from a registry to the local cache.

    .DESCRIPTION
    Reads cdf-packages.json from the current directory (or explicit path), resolves semver ranges
    against the registry, downloads packages to the local cache, and sets CDF_INFRA_TEMPLATES_PATH
    and CDF_INFRA_SOURCE_PATH environment variables.

    Can also install individual packages when -TemplateRef or -ConfigRef is specified.

    .PARAMETER ManifestPath
    Path to cdf-packages.json. Defaults to ./cdf-packages.json.

    .PARAMETER TemplateRef
    Install a single template by reference (e.g. 'platform/cas/v2pub:2.1.0' or 'platform/cas/v2pub:^2.0.0').

    .PARAMETER ConfigRef
    Install a single config by reference (e.g. 'tsdc01:1.3.0' or 'tsdc01:^1.0.0').

    .PARAMETER Registry
    Registry endpoint override. Required when using -TemplateRef or -ConfigRef without a manifest.

    .PARAMETER Force
    Re-download packages even if already cached.

    .EXAMPLE
    Install-CdfPackage
    # Reads ./cdf-packages.json and installs all declared packages

    .EXAMPLE
    Install-CdfPackage -TemplateRef 'platform/cas/v2pub:^2.1.0' -Registry cdfcodex.azurecr.io

    .LINK
    Publish-CdfTemplate
    .LINK
    Get-CdfPackage
    .LINK
    Test-CdfDependency
    #>


    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $false)]
        [string]$ManifestPath = './cdf-packages.json',
        [Parameter(Mandatory = $false)]
        [string]$TemplateRef,
        [Parameter(Mandatory = $false)]
        [string]$ConfigRef,
        [Parameter(Mandatory = $false)]
        [string]$Registry,
        [Parameter(Mandatory = $false)]
        [switch]$Force
    )

    # Single package install mode
    if ($TemplateRef -or $ConfigRef) {
        $inlineRegistries = $null
        if (Test-Path $ManifestPath) {
            $manifest = Get-Content -Raw $ManifestPath | ConvertFrom-Json -AsHashtable
            $inlineRegistries = $manifest.registries
        }
        if ($Registry -and $Registry -match '\.') {
            $regConfig = @{ type = 'acr'; endpoint = $Registry }
        }
        else {
            $regName = if ($Registry) { $Registry } else { 'default' }
            $regConfig = Resolve-CdfRegistryConfig -Name $regName -InlineRegistries $inlineRegistries
        }

        $provider = New-CdfRegistryProvider $regConfig
        $provider.Login()
        $endpoint = $regConfig.endpoint

        if ($TemplateRef) {
            Install-CdfSinglePackage -Ref $TemplateRef -PackageType 'templates' -Provider $provider -Endpoint $endpoint -Force:$Force
        }
        if ($ConfigRef) {
            Install-CdfSinglePackage -Ref $ConfigRef -PackageType 'configs' -Provider $provider -Endpoint $endpoint -Force:$Force
        }
        return
    }

    # Manifest-based install
    if (-not (Test-Path $ManifestPath)) {
        throw "No cdf-packages.json found at '$ManifestPath'. Use -ManifestPath or create one."
    }

    $manifest = Get-Content -Raw $ManifestPath | ConvertFrom-Json -AsHashtable
    $inlineRegistries = $manifest.registries
    $providers = @{}
    $defaultEndpoint = $null

    # Collect all registry names referenced by packages
    $referencedRegistries = @('default')
    if ($manifest.templates) {
        foreach ($key in $manifest.templates.Keys) {
            $parsed = Split-CdfPackageRef $key
            if ($parsed.Registry) { $referencedRegistries += $parsed.Registry }
        }
    }
    if ($manifest.configs) {
        foreach ($key in $manifest.configs.Keys) {
            $parsed = Split-CdfPackageRef $key
            if ($parsed.Registry) { $referencedRegistries += $parsed.Registry }
        }
    }
    $referencedRegistries = $referencedRegistries | Select-Object -Unique

    # Initialize registry providers via layered resolution
    foreach ($regName in $referencedRegistries) {
        $regConfig = Resolve-CdfRegistryConfig -Name $regName -InlineRegistries $inlineRegistries
        $providers[$regName] = New-CdfRegistryProvider $regConfig
        if ($regName -eq 'default') {
            $defaultEndpoint = $regConfig.endpoint
        }
    }

    # Login to all registries
    foreach ($provider in $providers.Values) {
        $provider.Login()
    }

    $installedTemplates = @()
    $installedConfigs = @()

    # Install templates
    if ($manifest.templates) {
        foreach ($templateKey in $manifest.templates.Keys) {
            $range = $manifest.templates[$templateKey]

            # Parse registry from key (e.g. 'platform/cas/v2pub@custx')
            $parsed = Split-CdfPackageRef $templateKey
            $packagePath = $parsed.Path
            $regName = $parsed.Registry ?? 'default'
            $provider = $providers[$regName]
            $endpoint = $provider.Endpoint

            $registryPath = "cdf/templates/$packagePath"
            $availableReleases = $provider.ListReleases($registryPath)

            if (-not $availableReleases) {
                Write-Warning "No releases found for template '$packagePath' in registry '$endpoint'"
                continue
            }

            $bestRelease = Resolve-CdfBestRelease -AvailableReleases $availableReleases -Range $range
            if (-not $bestRelease) {
                Write-Warning "No release matching '$range' found for template '$packagePath'. Available: $($availableReleases -join ', ')"
                continue
            }

            $cached = Get-CdfCachedPackage -PackageType 'templates' -Endpoint $endpoint -PackagePath $packagePath -Release $bestRelease
            if ($cached.Cached -and -not $Force) {
                Write-Host "Template ${packagePath}:$bestRelease already cached."
            }
            else {
                if ($Force -and $cached.Cached) {
                    Remove-Item -Recurse -Force $cached.Path
                }
                Save-CdfPackageToCache -PackageType 'templates' -Endpoint $endpoint -PackagePath $packagePath -Release $bestRelease -Provider $provider
            }
            $installedTemplates += @{ Path = $packagePath; Release = $bestRelease; Endpoint = $endpoint }
        }
    }

    # Install configs
    if ($manifest.configs) {
        foreach ($configKey in $manifest.configs.Keys) {
            $range = $manifest.configs[$configKey]

            $parsed = Split-CdfPackageRef $configKey
            $packagePath = $parsed.Path
            $regName = $parsed.Registry ?? 'default'
            $provider = $providers[$regName]
            $endpoint = $provider.Endpoint

            $registryPath = "cdf/configs/$packagePath"
            $availableReleases = $provider.ListReleases($registryPath)

            if (-not $availableReleases) {
                Write-Warning "No releases found for config '$packagePath' in registry '$endpoint'"
                continue
            }

            $bestRelease = Resolve-CdfBestRelease -AvailableReleases $availableReleases -Range $range
            if (-not $bestRelease) {
                Write-Warning "No release matching '$range' found for config '$packagePath'. Available: $($availableReleases -join ', ')"
                continue
            }

            $cached = Get-CdfCachedPackage -PackageType 'configs' -Endpoint $endpoint -PackagePath $packagePath -Release $bestRelease
            if ($cached.Cached -and -not $Force) {
                Write-Host "Config ${packagePath}:$bestRelease already cached."
            }
            else {
                if ($Force -and $cached.Cached) {
                    Remove-Item -Recurse -Force $cached.Path
                }
                Save-CdfPackageToCache -PackageType 'configs' -Endpoint $endpoint -PackagePath $packagePath -Release $bestRelease -Provider $provider
            }
            $installedConfigs += @{ Path = $packagePath; Release = $bestRelease; Endpoint = $endpoint }
        }
    }

    # Set environment variables for backwards compatibility
    if ($installedTemplates.Count -gt 0 -and $defaultEndpoint) {
        $cacheRoot = Get-CdfPackageCacheRoot
        $templatesPath = Join-Path $cacheRoot "templates/$defaultEndpoint"
        $env:CDF_INFRA_TEMPLATES_PATH = $templatesPath
        Write-Host "Set CDF_INFRA_TEMPLATES_PATH=$templatesPath"
    }

    if ($installedConfigs.Count -gt 0) {
        $firstConfig = $installedConfigs[0]
        $cacheRoot = Get-CdfPackageCacheRoot
        $configPath = Join-Path $cacheRoot "configs/$($firstConfig.Endpoint)/$($firstConfig.Path)/$($firstConfig.Release)"
        $env:CDF_INFRA_SOURCE_PATH = $configPath
        Write-Host "Set CDF_INFRA_SOURCE_PATH=$configPath"
    }

    # Summary
    Write-Host "`nInstalled $($installedTemplates.Count) template(s) and $($installedConfigs.Count) config(s)."

    # Run dependency check
    if ($installedTemplates.Count -gt 0) {
        Test-Dependency -Silent:$false
    }
}

# Helper: Parse package ref with optional @registry suffix
Function Split-CdfPackageRef {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)]
        [string]$Ref
    )

    if ($Ref -match '^(.+)@([^@]+)$') {
        return @{
            Path     = $Matches[1]
            Registry = $Matches[2]
        }
    }
    return @{
        Path     = $Ref
        Registry = $null
    }
}

# Helper: Install a single package from a ref like 'platform/cas/v2pub:^2.1.0'
Function Install-CdfSinglePackage {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true)]
        [string]$Ref,
        [Parameter(Mandatory = $true)]
        [ValidateSet('templates', 'configs')]
        [string]$PackageType,
        [Parameter(Mandatory = $true)]
        [CdfRegistryProvider]$Provider,
        [Parameter(Mandatory = $true)]
        [string]$Endpoint,
        [Parameter(Mandatory = $false)]
        [switch]$Force
    )

    if ($Ref -notmatch '^(.+):(.+)$') {
        throw "Invalid package reference '$Ref'. Expected format: '<path>:<release-or-range>'"
    }
    $packagePath = $Matches[1]
    $releaseOrRange = $Matches[2]

    $registryPath = "cdf/$PackageType/$packagePath"

    # If exact version, use directly; otherwise resolve from registry
    if ($releaseOrRange -match '^\d+\.\d+\.\d+') {
        $release = $releaseOrRange
    }
    else {
        $availableReleases = $Provider.ListReleases($registryPath)
        $release = Resolve-CdfBestRelease -AvailableReleases $availableReleases -Range $releaseOrRange
        if (-not $release) {
            throw "No release matching '$releaseOrRange' for '$packagePath'"
        }
    }

    $cached = Get-CdfCachedPackage -PackageType $PackageType -Endpoint $Endpoint -PackagePath $packagePath -Release $release
    if ($cached.Cached -and -not $Force) {
        Write-Host "$PackageType/${packagePath}:$release already cached at $($cached.Path)"
        return
    }
    if ($Force -and $cached.Cached) {
        Remove-Item -Recurse -Force $cached.Path
    }

    Save-CdfPackageToCache -PackageType $PackageType -Endpoint $Endpoint -PackagePath $packagePath -Release $release -Provider $Provider
    Write-Host "Installed $PackageType/${packagePath}:$release"
}