Modules/businessdev.ALbuild.Core/Public/Get-ALbuildConfig.ps1

function Get-ALbuildConfig {
    <#
    .SYNOPSIS
        Returns the effective ALbuild machine/runtime configuration.
 
    .DESCRIPTION
        Machine-scoped tooling settings (cache folders, the AL Tool store, retry, telemetry, licensing) -
        NOT the committed, per-workspace project settings (country, artifact type, test runner, ...), which
        are loaded with Get-ALbuildProjectConfig.
 
        Builds a [ALbuildConfig] instance by merging, in increasing precedence:
          1. Built-in defaults (the ALbuildConfig constructor)
          2. The PER-USER config file, if present (legacy location)
          3. The MACHINE-WIDE config file, if present
          4. In-memory overrides set with Set-ALbuildConfig
 
        The machine file deliberately outranks the per-user one. These are properties of the SERVER - which
        drive holds the artifact cache, where the AL Tool lives - so an administrator's system-wide setting
        must not be silently undone by a file in some account's profile. That precedence is what makes the
        setting trustworthy on a shared build agent; a developer box can still keep a personal file, it just
        loses against an explicit machine setting.
 
        ALBUILD_CONFIG names one explicit file and replaces the layering entirely.
 
        Why it matters: the per-user file used to be the only store, so the agent account could hold
        'G:\alb' in its own profile while an administrator on the same machine read the built-in default
        'C:\alb' - two different caches on one server, with nothing pointing out the split. Both halves of
        that trap are now closed: the machine file is the canonical store, and a config that exists only
        per-user is reported as such instead of passing for a machine setting.
 
        The merged instance is cached for the session; use -Refresh to rebuild it (for example after editing
        a config file on disk).
 
    .PARAMETER Name
        Optional single setting name to return instead of the whole object.
 
    .PARAMETER Refresh
        Rebuilds the cached configuration from defaults + files + overrides.
 
    .PARAMETER Source
        Returns where the configuration comes from instead of the values: the candidate files, whether they
        exist, and which settings each one contributes. Use it when a value is not what you expect - the
        answer is almost always "a different file than you think".
 
    .EXAMPLE
        Get-ALbuildConfig
 
    .EXAMPLE
        Get-ALbuildConfig -Name ArtifactCacheFolder
 
    .EXAMPLE
        Get-ALbuildConfig -Source
        Shows the machine and per-user file paths, which exist, and the settings each provides.
 
    .OUTPUTS
        ALbuildConfig, the requested setting value, or (with -Source) a PSCustomObject describing the
        resolution.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [string] $Name,

        [switch] $Refresh,

        [switch] $Source
    )

    # The layer files, in increasing precedence. An explicit ALBUILD_CONFIG replaces both.
    $envPath = if (-not [string]::IsNullOrWhiteSpace($env:ALBUILD_CONFIG)) { $env:ALBUILD_CONFIG } else { $null }
    $userPath = if ($envPath) { $null } else { Get-ALbuildConfigPath -Scope User }
    $machinePath = if ($envPath) { $null } else { Get-ALbuildConfigPath -Scope Machine }

    $readLayer = {
        param([string] $path)
        # Returns a hashtable of the file's settings, or $null when there is nothing usable to read.
        if ([string]::IsNullOrWhiteSpace($path) -or -not (Test-Path -LiteralPath $path)) { return $null }
        try {
            $parsed = Get-Content -LiteralPath $path -Raw -Encoding UTF8 | ConvertFrom-Json
            $hash = @{}
            foreach ($property in $parsed.PSObject.Properties) { $hash[$property.Name] = $property.Value }
            return $hash
        }
        catch {
            Write-Warning "ALbuild: ignoring invalid config file '$path': $($_.Exception.Message)"
            return $null
        }
    }

    if ($Refresh -or $Source -or -not $script:ALbuildConfig) {
        $config = [ALbuildConfig]::new()

        $layers = [System.Collections.Generic.List[object]]::new()
        foreach ($layer in @(
                @{ Scope = 'Environment'; Path = $envPath },
                @{ Scope = 'User'; Path = $userPath },
                @{ Scope = 'Machine'; Path = $machinePath })) {
            if (-not $layer.Path) { continue }
            $settings = & $readLayer $layer.Path
            $layers.Add([PSCustomObject]@{
                    Scope    = $layer.Scope
                    Path     = $layer.Path
                    Exists   = [bool] (Test-Path -LiteralPath $layer.Path)
                    Settings = $settings
                })
            if ($settings) { $config.Apply($settings, $true) }
        }

        # Layer 4: in-memory overrides.
        if ($script:ALbuildConfigOverrides -and $script:ALbuildConfigOverrides.Count -gt 0) {
            $config.Apply($script:ALbuildConfigOverrides, $false)
        }

        $script:ALbuildConfig = $config
        $script:ALbuildConfigLayers = $layers

        # Diagnose the split that caused this to exist. Once per session, because the config is cached.
        $userLayer = @($layers | Where-Object { $_.Scope -eq 'User' -and $_.Settings }) | Select-Object -First 1
        $machineLayer = @($layers | Where-Object { $_.Scope -eq 'Machine' -and $_.Settings }) | Select-Object -First 1
        if ($userLayer -and -not $machineLayer) {
            Write-Warning ("ALbuild is configured PER USER only ('$($userLayer.Path)'). Other accounts on this " +
                'machine - build agents, scheduled tasks, another administrator - will not see these settings and ' +
                'will fall back to the built-in defaults, which is how one server ends up with two artifact ' +
                "caches. Make it machine-wide with: Set-ALbuildConfig -Settings @{ ... } -Persist (elevated).")
        }
        elseif ($userLayer -and $machineLayer) {
            $shadowed = @($userLayer.Settings.Keys | Where-Object {
                    $machineLayer.Settings.ContainsKey($_) -and
                    "$($machineLayer.Settings[$_])" -ne "$($userLayer.Settings[$_])"
                })
            if ($shadowed.Count -gt 0) {
                Write-Warning ("ALbuild: the machine configuration '$($machinePath)' overrides the per-user file " +
                    "'$($userLayer.Path)' for: $($shadowed -join ', '). The machine setting wins. Remove the " +
                    'per-user file to stop the two from disagreeing.')
            }
        }
    }

    if ($Source) {
        return [PSCustomObject]@{
            EffectiveFrom = @($script:ALbuildConfigLayers | Where-Object { $_.Settings } | Select-Object -ExpandProperty Scope)
            Layers        = @($script:ALbuildConfigLayers | ForEach-Object {
                    [PSCustomObject]@{
                        Scope    = $_.Scope
                        Path     = $_.Path
                        Exists   = $_.Exists
                        Settings = if ($_.Settings) { @($_.Settings.Keys | Sort-Object) } else { @() }
                    }
                })
            Overrides     = if ($script:ALbuildConfigOverrides) { @($script:ALbuildConfigOverrides.Keys | Sort-Object) } else { @() }
            Effective     = $script:ALbuildConfig.ToOrderedDictionary()
        }
    }

    if ($PSBoundParameters.ContainsKey('Name')) {
        return $script:ALbuildConfig.Get($Name)
    }

    return $script:ALbuildConfig
}