# Config.ps1 — config.json and state.json read/write helpers # Requires $InternalRoot to be defined in the calling script's scope before dot-sourcing. function Get-ConfigPath { Join-Path $InternalRoot 'data\config.json' } function Get-StatePath { Join-Path $InternalRoot 'data\state\state.json' } # ── Config ──────────────────────────────────────────────────────────────────── function Get-Config { $path = Get-ConfigPath if (-not (Test-Path $path)) { return @{ features = @{} } } try { $raw = Get-Content -Path $path -Raw -Encoding UTF8 -ErrorAction Stop return ConvertTo-DeepHashtable ($raw | ConvertFrom-Json) } catch { Write-Log -Level Error -Message "Failed to read config.json, returning empty defaults: $_" -Feature 'Config' return @{ features = @{} } } } function Save-Config { param([hashtable]$Config) $path = Get-ConfigPath $dir = Split-Path $path -Parent if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } try { $Config | ConvertTo-Json -Depth 10 | Set-Content -Path $path -Encoding UTF8 -ErrorAction Stop } catch { Write-Log -Level Error -Message "Failed to save config.json: $_" -Feature 'Config' } } # ── State ───────────────────────────────────────────────────────────────────── function Get-State { $path = Get-StatePath if (-not (Test-Path $path)) { return @{} } try { $raw = Get-Content -Path $path -Raw -Encoding UTF8 -ErrorAction Stop return ConvertTo-DeepHashtable ($raw | ConvertFrom-Json) } catch { Write-Log -Level Error -Message "Failed to read state.json, returning empty state: $_" -Feature 'Config' return @{} } } function Save-State { param([hashtable]$State) $path = Get-StatePath $dir = Split-Path $path -Parent if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } try { $State | ConvertTo-Json -Depth 10 | Set-Content -Path $path -Encoding UTF8 -ErrorAction Stop } catch { Write-Log -Level Error -Message "Failed to save state.json: $_" -Feature 'Config' } } # ── Feature config seeding ──────────────────────────────────────────────────── function Ensure-FeatureConfig { <# .SYNOPSIS Ensures config.json contains an entry for the given feature, with all settings keys present. Missing keys are filled from $FeatureMeta.Settings[*].Default. Returns the (potentially modified) config hashtable. #> param( [hashtable]$Config, [hashtable]$FeatureMeta ) $name = $FeatureMeta.Name if (-not $Config.features.ContainsKey($name)) { $Config.features[$name] = @{ enabled = $false } } foreach ($setting in $FeatureMeta.Settings) { if (-not $Config.features[$name].ContainsKey($setting.Key)) { $Config.features[$name][$setting.Key] = $setting.Default } } return $Config } # ── Internal helper ─────────────────────────────────────────────────────────── function ConvertTo-DeepHashtable { <# .SYNOPSIS Recursively converts PSCustomObject (from ConvertFrom-Json) into hashtables so all config/state values are mutable and support .ContainsKey(). #> param([object]$InputObject) if ($null -eq $InputObject) { return $null } if ($InputObject -is [hashtable]) { return $InputObject } if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { return @($InputObject | ForEach-Object { ConvertTo-DeepHashtable $_ }) } if ($InputObject -is [PSCustomObject]) { $hash = @{} foreach ($prop in $InputObject.PSObject.Properties) { $hash[$prop.Name] = ConvertTo-DeepHashtable $prop.Value } return $hash } return $InputObject }