private/Config.ps1
|
function Get-NitrogenMemberValue { param ( [Parameter(Mandatory = $false)]$InputObject, [Parameter(Mandatory = $true)][string]$Name ) if ($null -eq $InputObject) { return $null } if ($InputObject -is [System.Collections.IDictionary]) { if ($InputObject.Contains($Name)) { return $InputObject[$Name] } return $null } $prop = $InputObject.PSObject.Properties[$Name] if ($null -ne $prop) { return $prop.Value } return $null } function ConvertTo-NitrogenHashtable { param ( [Parameter(Mandatory = $true)]$InputObject ) if ($null -eq $InputObject) { return $null } if ($InputObject -is [System.Collections.IDictionary]) { $result = @{} foreach ($key in $InputObject.Keys) { $result[$key] = ConvertTo-NitrogenHashtable -InputObject $InputObject[$key] } return $result } if ($InputObject -is [System.Collections.IEnumerable] -and -not ($InputObject -is [string])) { $list = New-Object System.Collections.ArrayList foreach ($item in $InputObject) { [void]$list.Add((ConvertTo-NitrogenHashtable -InputObject $item)) } return @($list) } if ($InputObject -is [psobject] -and $InputObject.PSObject.Properties.Count -gt 0) { $result = @{} foreach ($property in $InputObject.PSObject.Properties) { $result[$property.Name] = ConvertTo-NitrogenHashtable -InputObject $property.Value } return $result } return $InputObject } function Merge-NitrogenHashtable { param ( [Parameter(Mandatory = $true)][hashtable]$Base, [Parameter(Mandatory = $true)][hashtable]$Overlay ) $merged = @{} foreach ($key in $Base.Keys) { $merged[$key] = $Base[$key] } foreach ($key in $Overlay.Keys) { $baseValue = Get-NitrogenMemberValue -InputObject $merged -Name $key $overlayValue = $Overlay[$key] if ($baseValue -is [hashtable] -and $overlayValue -is [hashtable]) { $merged[$key] = Merge-NitrogenHashtable -Base $baseValue -Overlay $overlayValue continue } $merged[$key] = $overlayValue } return $merged } function Get-NitrogenConfigPath { [CmdletBinding()] param ( [Parameter(Mandatory = $false)][ValidateSet('Global', 'Local')][string]$Scope = 'Global' ) if ($Scope -eq 'Local') { return (Join-Path -Path (Get-Location).Path -ChildPath '.nitrogen/config.json') } $homePath = if ($IsWindows) { $env:USERPROFILE } else { $env:HOME } if ([string]::IsNullOrWhiteSpace($homePath)) { throw "Unable to determine a home directory for global Nitrogen config path." } return (Join-Path -Path $homePath -ChildPath '.nitrogen/config.json') } function Read-NitrogenConfigFile { [CmdletBinding()] param ( [Parameter(Mandatory = $true)][string]$Path ) if (-not (Test-Path -Path $Path -PathType Leaf)) { return @{} } try { $content = Get-Content -Path $Path -Raw -ErrorAction Stop if ([string]::IsNullOrWhiteSpace($content)) { return @{} } $parsed = $content | ConvertFrom-Json -ErrorAction Stop $result = ConvertTo-NitrogenHashtable -InputObject $parsed if ($result -isnot [hashtable]) { throw "Configuration root must be a JSON object." } return $result } catch { throw "Failed to parse config file '$Path': $($_.Exception.Message)" } } function Test-NitrogenConfig { [CmdletBinding()] param ( [Parameter(Mandatory = $true)][hashtable]$Config, [Parameter(Mandatory = $true)][string]$Source ) $version = Get-NitrogenMemberValue -InputObject $Config -Name 'version' if ($null -ne $version -and [int]$version -ne 1) { throw "Unsupported config version '$version' in '$Source'. Supported version is 1." } $profiles = Get-NitrogenMemberValue -InputObject $Config -Name 'profiles' if ($null -eq $profiles) { return $true } if ($profiles -isnot [hashtable]) { throw "Invalid config in '$Source': 'profiles' must be an object." } $prompts = Get-NitrogenMemberValue -InputObject $Config -Name 'prompts' foreach ($profileName in $profiles.Keys) { $profileConfig = $profiles[$profileName] if ($profileConfig -isnot [hashtable]) { throw "Invalid config in '$Source': profile '$profileName' must be an object." } $promptRef = Get-NitrogenMemberValue -InputObject $profileConfig -Name 'prompt' if (-not [string]::IsNullOrWhiteSpace($promptRef)) { if ($prompts -isnot [hashtable] -or -not $prompts.Contains($promptRef)) { throw "Invalid config in '$Source': profile '$profileName' references unknown prompt '$promptRef'." } } $providers = Get-NitrogenMemberValue -InputObject $profileConfig -Name 'providers' if ($null -eq $providers) { continue } if ($providers -isnot [hashtable]) { throw "Invalid config in '$Source': profile '$profileName' providers must be an object." } foreach ($providerName in $providers.Keys) { $providerConfig = $providers[$providerName] if ($providerConfig -isnot [hashtable]) { throw "Invalid config in '$Source': provider '$providerName' in profile '$profileName' must be an object." } if ($providerConfig.Contains('apiKey')) { throw "Invalid config in '$Source': provider '$providerName' in profile '$profileName' uses plaintext 'apiKey'. Use 'apiKeyRef', environment variables, or -ApiKey." } $endpoint = Get-NitrogenMemberValue -InputObject $providerConfig -Name 'endpoint' if (-not [string]::IsNullOrWhiteSpace($endpoint)) { if ($endpoint -match '<[^>]+>') { continue } $uri = $null if (-not [Uri]::TryCreate($endpoint, [UriKind]::Absolute, [ref]$uri)) { throw "Invalid config in '$Source': provider '$providerName' in profile '$profileName' has invalid endpoint '$endpoint'." } if ($uri.Scheme -ne 'https') { throw "Invalid config in '$Source': provider '$providerName' in profile '$profileName' endpoint must use https." } } } } return $true } function Get-NitrogenEffectiveConfig { [CmdletBinding()] param ( [Parameter(Mandatory = $false)][string]$ConfigPath ) $globalPath = Get-NitrogenConfigPath -Scope Global $localPath = Get-NitrogenConfigPath -Scope Local $config = @{} $config = Merge-NitrogenHashtable -Base $config -Overlay (Read-NitrogenConfigFile -Path $globalPath) $config = Merge-NitrogenHashtable -Base $config -Overlay (Read-NitrogenConfigFile -Path $localPath) if (-not [string]::IsNullOrWhiteSpace($ConfigPath)) { $customResolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ConfigPath) $config = Merge-NitrogenHashtable -Base $config -Overlay (Read-NitrogenConfigFile -Path $customResolvedPath) } Test-NitrogenConfig -Config $config -Source "effective configuration" | Out-Null return $config } function Resolve-NitrogenProfileConfig { [CmdletBinding()] param ( [Parameter(Mandatory = $true)][hashtable]$Config, [Parameter(Mandatory = $false)][string]$Profile ) $defaults = Get-NitrogenMemberValue -InputObject $Config -Name 'defaults' $defaultProfile = Get-NitrogenMemberValue -InputObject $defaults -Name 'profile' $resolvedProfile = if (-not [string]::IsNullOrWhiteSpace($Profile)) { $Profile } elseif (-not [string]::IsNullOrWhiteSpace($defaultProfile)) { $defaultProfile } else { 'default' } $profiles = Get-NitrogenMemberValue -InputObject $Config -Name 'profiles' if ($profiles -isnot [hashtable] -or -not $profiles.Contains($resolvedProfile)) { if (-not [string]::IsNullOrWhiteSpace($Profile) -or ($profiles -is [hashtable] -and $profiles.Keys.Count -gt 0)) { $available = if ($profiles -is [hashtable]) { ($profiles.Keys -join ', ') } else { '<none>' } throw "Profile '$resolvedProfile' was not found in config. Available profiles: $available" } return [pscustomobject]@{ Name = $resolvedProfile Config = @{} } } return [pscustomobject]@{ Name = $resolvedProfile Config = $profiles[$resolvedProfile] } } function Resolve-NitrogenProviderConfig { [CmdletBinding()] param ( [Parameter(Mandatory = $true)][hashtable]$ProfileConfig, [Parameter(Mandatory = $false)][string]$Provider ) $providers = Get-NitrogenMemberValue -InputObject $ProfileConfig -Name 'providers' if ([string]::IsNullOrWhiteSpace($Provider)) { return @{} } if ($providers -is [hashtable] -and $providers.Contains($Provider)) { return $providers[$Provider] } return @{} } function Get-NitrogenPromptText { [CmdletBinding()] param ( [Parameter(Mandatory = $true)][hashtable]$Config, [Parameter(Mandatory = $true)][hashtable]$ProfileConfig ) $promptRef = Get-NitrogenMemberValue -InputObject $ProfileConfig -Name 'prompt' if ([string]::IsNullOrWhiteSpace($promptRef)) { return $null } $prompts = Get-NitrogenMemberValue -InputObject $Config -Name 'prompts' if ($prompts -isnot [hashtable] -or -not $prompts.Contains($promptRef)) { throw "Prompt reference '$promptRef' does not exist in config 'prompts'." } return $prompts[$promptRef] } function Get-NitrogenApiKey { [CmdletBinding()] param ( [Parameter(Mandatory = $true)][ValidateSet('OpenAI', 'Anthropic', 'AzureOpenAI', 'Google', 'Perplexity')][string]$Provider, [Parameter(Mandatory = $false)][hashtable]$ProviderConfig = @{} ) $envVarName = switch ($Provider) { 'OpenAI' { 'OPENAI_API_KEY' } 'Anthropic' { 'ANTHROPIC_API_KEY' } 'AzureOpenAI'{ 'AZURE_OPENAI_API_KEY' } 'Google' { 'GEMINI_API_KEY' } 'Perplexity' { 'PERPLEXITY_API_KEY' } } $envValue = [Environment]::GetEnvironmentVariable($envVarName) if (-not [string]::IsNullOrWhiteSpace($envValue)) { return $envValue } $secretRef = Get-NitrogenMemberValue -InputObject $ProviderConfig -Name 'apiKeyRef' if ([string]::IsNullOrWhiteSpace($secretRef)) { return $null } $getSecret = Get-Command -Name Get-Secret -ErrorAction SilentlyContinue if ($null -eq $getSecret) { throw "Provider '$Provider' is configured with apiKeyRef '$secretRef', but Get-Secret is unavailable. Install Microsoft.PowerShell.SecretManagement and register a vault." } try { $secret = Get-Secret -Name $secretRef -ErrorAction Stop } catch { throw "Failed to retrieve secret '$secretRef' for provider '$Provider': $($_.Exception.Message)" } if ($secret -is [securestring]) { $ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secret) try { $secret = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr) } } if ([string]::IsNullOrWhiteSpace([string]$secret)) { throw "Secret '$secretRef' for provider '$Provider' is empty." } return [string]$secret } |