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

function Set-ALbuildConfig {
    <#
    .SYNOPSIS
        Sets one or more ALbuild configuration values.
 
    .DESCRIPTION
        Applies overrides on top of the current configuration. By default the override lives for the
        session only; with -Persist it is written to a config file so it survives future sessions.
 
        -Persist writes the MACHINE-WIDE file by default. These are properties of the server - which drive
        holds the artifact cache, where the AL Tool lives - and every account has to agree on them. Writing
        them per user is what let one server carry two artifact caches: the agent account had 'G:\alb' in
        its own profile while an administrator read the built-in default 'C:\alb'.
 
        Only settings that DIFFER from the built-in defaults are written. The file therefore stays a short,
        readable statement of what this machine does differently, and a value ALbuild changes in a later
        release (a licensing endpoint, a retry count) still reaches the agent instead of being frozen by a
        file that once captured every key.
 
    .PARAMETER Settings
        A hashtable of setting name/value pairs to apply.
 
    .PARAMETER Persist
        Persists the configuration to the file for -Scope.
 
    .PARAMETER Scope
        Machine (default) - the shared file under the common application-data folder
        ('C:\ProgramData\ALbuild\config.json'). Requires elevation on Windows, which is the point: a
        machine-wide setting should take an administrator.
 
        User - the per-user file under the roaming application-data folder. Ranks BELOW the machine file
        when both exist (see Get-ALbuildConfig), so use it for a personal developer box, never to configure
        a build agent.
 
        ALBUILD_CONFIG, when set, overrides both.
 
    .EXAMPLE
        Set-ALbuildConfig -Settings @{ ProcessRetryCount = 5; TelemetryEnabled = $true }
 
    .EXAMPLE
        Set-ALbuildConfig -Settings @{ BaseFolder = 'G:\alb'; ArtifactCacheFolder = 'G:\alb\artifacts'; PackageCacheFolder = 'G:\alb\packages'; BcArtifactCacheFolder = 'G:\bcartifacts.cache' } -Persist
        Points the whole machine at drive G:. Run elevated; every account then sees it.
 
    .OUTPUTS
        System.Collections.Specialized.OrderedDictionary (the updated configuration).
    #>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')]
    param(
        [Parameter(Mandatory, Position = 0)]
        [ValidateNotNull()]
        [hashtable] $Settings,

        [switch] $Persist,

        [ValidateSet('Machine', 'User')]
        [string] $Scope = 'Machine'
    )

    if (-not $script:ALbuildConfigOverrides) {
        $script:ALbuildConfigOverrides = @{}
    }

    # Validate against the known settings before storing, so an invalid key never poisons state.
    $known = ([ALbuildConfig]::new()).Keys()
    foreach ($key in $Settings.Keys) {
        if ($known -notcontains $key) {
            throw "Unknown ALbuild configuration setting '$key'. Known settings: $($known -join ', ')."
        }
        $script:ALbuildConfigOverrides[$key] = $Settings[$key]
    }

    # Rebuild the cached effective config so callers see the change immediately.
    $effective = Get-ALbuildConfig -Refresh

    if ($Persist) {
        $configPath = Get-ALbuildConfigPath -Scope $Scope

        # Fail BEFORE touching anything, with the remedy in the message. Writing under ProgramData without
        # elevation otherwise ends in a bare UnauthorizedAccessException that says nothing about scope.
        if ($Scope -eq 'Machine' -and -not $env:ALBUILD_CONFIG -and -not (Test-ALbuildElevated)) {
            $who = try { [System.Security.Principal.WindowsIdentity]::GetCurrent().Name } catch { $env:USERNAME }
            throw ("Writing the machine-wide ALbuild configuration to '$configPath' requires an elevated " +
                "session (current user: $who). Start PowerShell as administrator, or use -Scope User for a " +
                'per-user setting - but note that a per-user file does NOT apply to build agents or other ' +
                'accounts on this machine.')
        }

        if ($PSCmdlet.ShouldProcess($configPath, "Persist ALbuild configuration ($Scope scope)")) {
            try {
                $dir = Split-Path -Path $configPath -Parent
                if (-not (Test-Path -LiteralPath $dir)) {
                    New-Item -Path $dir -ItemType Directory -Force | Out-Null
                }

                # Sparse: only what differs from the built-in defaults.
                $defaults = ([ALbuildConfig]::new()).ToOrderedDictionary()
                $current = $effective.ToOrderedDictionary()
                $toWrite = [ordered] @{}
                foreach ($key in $current.Keys) {
                    if ("$($current[$key])" -ne "$($defaults[$key])") { $toWrite[$key] = $current[$key] }
                }

                ($toWrite | ConvertTo-Json -Depth 10) | Set-Content -LiteralPath $configPath -Encoding UTF8
                Write-Verbose "ALbuild: persisted $($toWrite.Count) non-default setting(s) to '$configPath' ($Scope scope)."
            }
            catch {
                throw "Failed to persist ALbuild configuration to '$configPath': $($_.Exception.Message)"
            }
        }
    }

    return $effective
}