Napreth.PSDirEnv.psm1
|
Set-StrictMode -Version 3.0 $script:PdeEnabled = $false $script:PdeBusy = $false $script:PdeLayers = [System.Collections.Generic.List[object]]::new() $script:PdeProfiles = @() $script:PdeFailures = @{} $script:PdeNotices = [System.Collections.Generic.HashSet[string]]::new() $script:PdePrompt = $null $script:PdeOriginalPrompt = $null $script:PdePathComparer = if ($IsWindows) { [StringComparer]::OrdinalIgnoreCase } else { [StringComparer]::Ordinal } $script:PdeAutomaticVariables = @( '?', '^', '$', '_', 'args', 'ConsoleFileName', 'EnabledExperimentalFeatures', 'Error', 'Event', 'EventArgs', 'EventSubscriber', 'ExecutionContext', 'false', 'foreach', 'HOME', 'Host', 'input', 'IsCoreCLR', 'IsLinux', 'IsMacOS', 'IsWindows', 'LASTEXITCODE', 'Matches', 'MyInvocation', 'NestedPromptLevel', 'null', 'PID', 'PROFILE', 'PSBoundParameters', 'PSCommandPath', 'PSCulture', 'PSDebugContext', 'PSEdition', 'PSHOME', 'PSItem', 'PSScriptRoot', 'PSSenderInfo', 'PSUICulture', 'PSVersionTable', 'PWD', 'Sender', 'ShellId', 'StackTrace', 'switch', 'this', 'true', 'PSDefaultParameterValues' ) $script:PdeCommands = @('Enable-DirEnv', 'Disable-DirEnv', 'Approve-DirEnv', 'Revoke-DirEnv', 'Update-DirEnv', 'Get-DirEnv', 'prompt') $stateRoot = if ($IsWindows) { [Environment]::GetFolderPath('LocalApplicationData') } elseif ($env:XDG_STATE_HOME) { $env:XDG_STATE_HOME } else { Join-Path $HOME '.local/state' } $script:PdeStorePath = Join-Path $stateRoot 'PSDirEnv/approvals.json' Remove-Variable stateRoot function Format-PdeMessage { param([string]$Message, [switch]$Error) if ($Message.StartsWith('psdirenv: ')) { return $Message } $prefix = if ($Error) { 'psdirenv: error ' } else { 'psdirenv: ' } return $prefix + $Message } function Write-PdeLog { [CmdletBinding()] param([string]$Message, [switch]$Error, [switch]$Quiet) if (!$Quiet) { # Use the information stream for human-readable logs, keeping command # results and the original prompt's return value on the success stream. $text = Format-PdeMessage $Message -Error:$Error $color = $env:TERM -ne 'dumb' -and [string]::IsNullOrEmpty($env:NO_COLOR) -and $PSStyle.OutputRendering -ne 'PlainText' -and ($PSStyle.OutputRendering -eq 'Ansi' -or $Host.UI.SupportsVirtualTerminal) if ($color) { # direnv uses ANSI red for errors and resets normal status output. $text = if ($Error -or $text.StartsWith('psdirenv: error ')) { "`e[31m${text}`e[0m" } else { "`e[0m$text" } } Write-Information -MessageData $text -InformationAction Continue } } function Format-PdePath { param([string]$Path) $homePrefix = $HOME.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar if ($Path.StartsWith($homePrefix, $(if ($IsWindows) { [StringComparison]::OrdinalIgnoreCase } else { [StringComparison]::Ordinal }))) { return '~' + [IO.Path]::DirectorySeparatorChar + $Path.Substring($homePrefix.Length) } return $Path } function New-PdeMap { param([switch]$Environment) $comparer = if ($Environment) { $script:PdePathComparer } else { [StringComparer]::OrdinalIgnoreCase } return ,([hashtable]::new($comparer)) } function Test-PdeReservedName { param([string]$Name) return $Name -match '(^Pde|^.*-Pde)' -or $Name -in $script:PdeCommands } function Get-PdeSnapshot { $snapshot = @{ Environment = New-PdeMap -Environment Variable = New-PdeMap Function = New-PdeMap Alias = New-PdeMap } foreach ($entry in [Environment]::GetEnvironmentVariables('Process').GetEnumerator()) { $snapshot.Environment[$entry.Key] = [pscustomobject]@{ Value = [string]$entry.Value } } foreach ($variable in Get-Variable -Scope Global) { if ($variable.Name -in $script:PdeAutomaticVariables) { continue } $snapshot.Variable[$variable.Name] = [pscustomobject]@{ Value = $variable.Value; Options = $variable.Options; Description = $variable.Description } } # The provider exposes inherited global functions as well as this module's helpers. # Helpers are never published or changed by the engine; reserve their names below. foreach ($function in Get-ChildItem Function:) { $snapshot.Function[$function.Name] = [pscustomobject]@{ ScriptBlock = $function.ScriptBlock; Options = $function.Options; Description = $function.Description } } foreach ($alias in Get-Alias -Scope Global) { $snapshot.Alias[$alias.Name] = [pscustomobject]@{ Definition = $alias.Definition; Options = $alias.Options; Description = $alias.Description } } return $snapshot } function Test-PdeStateEqual { param([string]$Kind, $Left, $Right) if ($null -eq $Left -or $null -eq $Right) { return $null -eq $Left -and $null -eq $Right } switch ($Kind) { Environment { return [string]::Equals($Left.Value, $Right.Value, [StringComparison]::Ordinal) } Variable { return [object]::Equals($Left.Value, $Right.Value) -and $Left.Options -eq $Right.Options -and $Left.Description -eq $Right.Description } Function { return [object]::ReferenceEquals($Left.ScriptBlock, $Right.ScriptBlock) -and $Left.Options -eq $Right.Options -and $Left.Description -eq $Right.Description } Alias { return $Left.Definition -ceq $Right.Definition -and $Left.Options -eq $Right.Options -and $Left.Description -eq $Right.Description } } } function Get-PdeChanges { param($Before, $After) foreach ($kind in @('Environment', 'Variable', 'Function', 'Alias')) { $comparer = if ($kind -eq 'Environment') { $script:PdePathComparer } else { [StringComparer]::OrdinalIgnoreCase } $names = [System.Collections.Generic.HashSet[string]]::new($comparer) foreach ($name in $Before[$kind].Keys) { $null = $names.Add($name) } foreach ($name in $After[$kind].Keys) { $null = $names.Add($name) } foreach ($name in $names) { if (!(Test-PdeStateEqual $kind $Before[$kind][$name] $After[$kind][$name])) { [pscustomobject]@{ Kind = $kind; Name = $name; Before = $Before[$kind][$name]; After = $After[$kind][$name] } } } } } function Set-PdeState { param([string]$Kind, [string]$Name, $State) switch ($Kind) { Environment { if ($null -eq $State) { # PowerShell converts $null to an empty System.String at this # API boundary. NullString requests deletion on .NET 9+ too. [Environment]::SetEnvironmentVariable($Name, [System.Management.Automation.Language.NullString]::Value, 'Process') } else { [Environment]::SetEnvironmentVariable($Name, $State.Value, 'Process') } } Variable { if ($null -eq $State) { Remove-Variable -Scope Global -Name $Name -Force -ErrorAction Stop } else { Set-Variable -Scope Global -Name $Name -Value $State.Value -Option $State.Options -Description $State.Description -Force -ErrorAction Stop } } Function { # Remove uses the inherited name: Function:\global:name does not remove # a global function when invoked from a module on PowerShell 7. if ($null -eq $State) { Remove-Item -LiteralPath "Function:\$Name" -Force -ErrorAction Stop } else { Set-Item -LiteralPath "Function:\global:$Name" -Value $State.ScriptBlock -Options $State.Options -Force -ErrorAction Stop (Get-Item -LiteralPath "Function:\$Name").Description = $State.Description } } Alias { if ($null -eq $State) { Remove-Alias -Scope Global -Name $Name -Force -ErrorAction Stop } else { Set-Alias -Scope Global -Name $Name -Value $State.Definition -Option $State.Options -Description $State.Description -Force -ErrorAction Stop } } } } function Undo-PdeChanges { param([object[]]$Changes, [switch]$Unconditional) $current = Get-PdeSnapshot for ($index = $Changes.Count - 1; $index -ge 0; $index--) { $change = $Changes[$index] if ($Unconditional -or (Test-PdeStateEqual $change.Kind $current[$change.Kind][$change.Name] $change.After)) { Set-PdeState $change.Kind $change.Name $change.Before } } } function Write-PdeDiff { param($Before, $After, [switch]$Quiet) if ($Quiet) { return } $items = [System.Collections.Generic.List[string]]::new() foreach ($change in Get-PdeChanges $Before $After) { $sign = if ($null -eq $change.Before) { '+' } elseif ($null -eq $change.After) { '-' } else { '~' } $name = switch ($change.Kind) { Environment { $change.Name } Variable { '$' + $change.Name } Function { 'function:' + $change.Name } Alias { 'alias:' + $change.Name } } $items.Add($sign + $name) } if ($items.Count) { $items.Sort([StringComparer]::Ordinal) Write-PdeLog ('export ' + ($items -join ' ')) } } function Read-PdeApprovals { if (!(Test-Path -LiteralPath $script:PdeStorePath -PathType Leaf)) { return New-PdeMap -Environment } try { $store = Get-Content -LiteralPath $script:PdeStorePath -Raw -ErrorAction Stop | ConvertFrom-Json -AsHashtable -ErrorAction Stop } catch { throw (Format-PdeMessage "failed to parse ${script:PdeStorePath}: $($_.Exception.Message)" -Error) } if ($store -isnot [System.Collections.IDictionary] -or !$store.ContainsKey('Version') -or !$store.ContainsKey('Approvals') -or $store.Version -ne 1 -or $store.Approvals -isnot [System.Collections.IDictionary]) { throw (Format-PdeMessage "invalid approval store: $script:PdeStorePath" -Error) } $approvals = New-PdeMap -Environment foreach ($path in $store.Approvals.Keys) { if ($store.Approvals[$path] -isnot [string] -or $store.Approvals[$path] -notmatch '^[A-Fa-f0-9]{64}$') { throw (Format-PdeMessage "invalid approval entry: $path" -Error) } $approvals[$path] = $store.Approvals[$path] } return ,$approvals } function Write-PdeApproval { param([string]$Path, [AllowNull()][string]$Hash) $directory = Split-Path -Parent $script:PdeStorePath $null = New-Item -ItemType Directory -Path $directory -Force -ErrorAction Stop $lock = $null $temporaryPath = Join-Path $directory ('.approvals-' + [guid]::NewGuid().ToString('N') + '.tmp') try { for ($attempt = 0; $attempt -lt 40; $attempt++) { try { $lock = [IO.File]::Open($script:PdeStorePath + '.lock', 'OpenOrCreate', 'ReadWrite', 'None') break } catch [IO.IOException] { Start-Sleep -Milliseconds 50 } } if ($null -eq $lock) { throw (Format-PdeMessage 'approval store is busy; try again' -Error) } $approvals = Read-PdeApprovals if ([string]::IsNullOrEmpty($Hash)) { $approvals.Remove($Path) } else { $approvals[$Path] = $Hash } $json = @{ Version = 1; Approvals = $approvals } | ConvertTo-Json -Depth 4 [IO.File]::WriteAllText($temporaryPath, $json, [Text.UTF8Encoding]::new($false)) [IO.File]::Move($temporaryPath, $script:PdeStorePath, $true) } finally { if ($null -ne $lock) { $lock.Dispose() } if ([IO.File]::Exists($temporaryPath)) { [IO.File]::Delete($temporaryPath) } } } function Resolve-PdeProfilePath { param([string]$Path = '.') $resolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) if ([IO.Directory]::Exists($resolved)) { return [IO.Path]::Combine($resolved, '.ps.envrc') } if ([IO.Path]::GetFileName($resolved) -ne '.ps.envrc') { throw (Format-PdeMessage 'expected a directory or a .ps.envrc file' -Error) } return $resolved } function Read-PdeProfile { param([string]$Path) # Hash and execute the same bytes, so a write between approval checking and # evaluation cannot substitute different code. try { $bytes = [IO.File]::ReadAllBytes($Path) } catch [IO.FileNotFoundException] { throw (Format-PdeMessage '.ps.envrc file not found' -Error) } catch [IO.DirectoryNotFoundException] { throw (Format-PdeMessage '.ps.envrc file not found' -Error) } $hash = [Convert]::ToHexString([Security.Cryptography.SHA256]::HashData($bytes)) $stream = [IO.MemoryStream]::new($bytes, $false) $reader = [IO.StreamReader]::new($stream, [Text.UTF8Encoding]::new($false, $true), $true) try { $content = $reader.ReadToEnd() } finally { $reader.Dispose(); $stream.Dispose() } return [pscustomobject]@{ Path = $Path; Hash = $hash; Content = $content; Approved = $false; Status = 'Unapproved'; Error = $null } } function Get-PdeProfiles { $location = Get-Location if ($location.Provider.Name -ne 'FileSystem') { return } $directories = [System.Collections.Generic.List[string]]::new() $directory = [IO.DirectoryInfo]::new($location.ProviderPath) while ($null -ne $directory) { $directories.Insert(0, $directory.FullName); $directory = $directory.Parent } foreach ($path in $directories) { $file = [IO.Path]::Combine($path, '.ps.envrc') if ([IO.File]::Exists($file)) { Read-PdeProfile $file } } } function Add-PdeLayer { param($Profile) $before = Get-PdeSnapshot $location = Get-Location $module = $null try { $tokens = $null; $parseErrors = $null $ast = [System.Management.Automation.Language.Parser]::ParseInput($Profile.Content, $Profile.Path, [ref]$tokens, [ref]$parseErrors) if ($parseErrors.Count) { throw ($parseErrors.Message -join [Environment]::NewLine) } Set-Location -LiteralPath (Split-Path -Parent $Profile.Path) -ErrorAction Stop # A private module keeps functions bound to their .ps.envrc variables and # PSScriptRoot. Plain definitions do not need global: qualifiers. $module = New-Module -Name ('Napreth.PSDirEnv_Overlay_' + [guid]::NewGuid().ToString('N')) -ArgumentList $ast.GetScriptBlock() -ScriptBlock { param($PdeSource) $ErrorActionPreference = 'Stop' . $PdeSource # New-Module otherwise publishes exported functions into its caller's # scope, which would shadow globals inside the engine on later reads. Export-ModuleMember -Function @() -Alias @() -Variable @() Remove-Variable PdeSource, ErrorActionPreference } -ErrorAction Stop $definitions = & $module { [pscustomobject]@{ Variables = @(Get-Variable -Scope Script) Functions = @(Get-ChildItem Function: | Where-Object ModuleName -eq $ExecutionContext.SessionState.Module.Name) Aliases = @(Get-Alias -Scope Script | Where-Object ModuleName -eq $ExecutionContext.SessionState.Module.Name) } } foreach ($variable in $definitions.Variables) { if ($variable.Name -in $script:PdeAutomaticVariables) { continue } if (Test-PdeReservedName $variable.Name) { throw (Format-PdeMessage "reserved name: $($variable.Name)" -Error) } if ($variable.Options -band ([System.Management.Automation.ScopedItemOptions]::Constant -bor [System.Management.Automation.ScopedItemOptions]::AllScope)) { throw (Format-PdeMessage "cannot overlay Constant or AllScope variable: $($variable.Name)" -Error) } Set-PdeState Variable $variable.Name ([pscustomobject]@{ Value = $variable.Value; Options = $variable.Options; Description = $variable.Description }) } foreach ($function in $definitions.Functions) { if (Test-PdeReservedName $function.Name) { throw (Format-PdeMessage "reserved name: $($function.Name)" -Error) } if ($function.Options -band ([System.Management.Automation.ScopedItemOptions]::Constant -bor [System.Management.Automation.ScopedItemOptions]::AllScope)) { throw (Format-PdeMessage "cannot overlay Constant or AllScope function: $($function.Name)" -Error) } Set-PdeState Function $function.Name ([pscustomobject]@{ ScriptBlock = $function.ScriptBlock; Options = $function.Options; Description = $function.Description }) } foreach ($alias in $definitions.Aliases) { if (Test-PdeReservedName $alias.Name) { throw (Format-PdeMessage "reserved name: $($alias.Name)" -Error) } if ($alias.Options -band ([System.Management.Automation.ScopedItemOptions]::Constant -bor [System.Management.Automation.ScopedItemOptions]::AllScope)) { throw (Format-PdeMessage "cannot overlay Constant or AllScope alias: $($alias.Name)" -Error) } Set-PdeState Alias $alias.Name ([pscustomobject]@{ Definition = $alias.Definition; Options = $alias.Options; Description = $alias.Description }) } $changes = @(Get-PdeChanges $before (Get-PdeSnapshot)) foreach ($change in $changes) { if ($change.Kind -ne 'Environment' -and (Test-PdeReservedName $change.Name)) { throw (Format-PdeMessage "reserved name: $($change.Name)" -Error) } } $script:PdeLayers.Add([pscustomobject]@{ Path = $Profile.Path; Hash = $Profile.Hash; Changes = $changes; Module = $module }) } catch { $failure = $_ Undo-PdeChanges -Changes @(Get-PdeChanges $before (Get-PdeSnapshot)) -Unconditional throw $failure } finally { Set-Location -LiteralPath $location.Path -ErrorAction Stop } } function Remove-PdeLastLayer { $index = $script:PdeLayers.Count - 1 $layer = $script:PdeLayers[$index] Undo-PdeChanges -Changes $layer.Changes $script:PdeLayers.RemoveAt($index) # Dynamic modules are not imported into the module table. Releasing the # layer lets them be collected; manually retained functions keep their binding. } function Write-PdeNotice { param([string]$Key, [string]$Message, [switch]$Quiet) if (!$Quiet -and $script:PdeNotices.Add($Key)) { Write-PdeLog $Message -Error } } function Update-DirEnv { <# .SYNOPSIS Refresh .ps.envrc overlays for the current directory. Use -Force to retry or reload. #> [CmdletBinding()] param([switch]$Force, [switch]$Quiet) if (!$script:PdeEnabled -or $script:PdeBusy) { return } $script:PdeBusy = $true try { if ($Force) { $script:PdeFailures.Clear(); $script:PdeNotices.Clear() } try { $profiles = @(Get-PdeProfiles) $approvals = Read-PdeApprovals } catch { $before = if ($script:PdeLayers.Count -and !$Quiet) { Get-PdeSnapshot } else { $null } if ($script:PdeLayers.Count) { Write-PdeLog 'unloading' -Quiet:$Quiet } while ($script:PdeLayers.Count) { Remove-PdeLastLayer } if ($null -ne $before) { Write-PdeDiff $before (Get-PdeSnapshot) } $script:PdeProfiles = @() throw } $allowed = [System.Collections.Generic.List[object]]::new() $blocked = $false foreach ($profile in $profiles) { $profile.Approved = $approvals.ContainsKey($profile.Path) -and $approvals[$profile.Path] -ceq $profile.Hash if ($blocked) { $profile.Status = 'Blocked'; continue } if (!$profile.Approved) { $blocked = $true $approveCommand = "Approve-DirEnv -Path '$($profile.Path.Replace("'", "''"))'" $message = '{0} is blocked. Run `{1}` to approve its content' -f $profile.Path, $approveCommand Write-PdeNotice -Key ($profile.Path + ':' + $profile.Hash) -Message $message -Quiet:$Quiet continue } if ($script:PdeFailures.ContainsKey($profile.Path) -and $script:PdeFailures[$profile.Path].Hash -ceq $profile.Hash) { $profile.Status = 'Failed'; $profile.Error = $script:PdeFailures[$profile.Path].Error; $blocked = $true Write-PdeNotice -Key ('error:' + $profile.Path + ':' + $profile.Hash) -Message $profile.Error -Quiet:$Quiet continue } $profile.Status = 'Approved' $allowed.Add($profile) } $common = 0 if (!$Force) { while ($common -lt $script:PdeLayers.Count -and $common -lt $allowed.Count -and $script:PdePathComparer.Equals($script:PdeLayers[$common].Path, $allowed[$common].Path) -and $script:PdeLayers[$common].Hash -ceq $allowed[$common].Hash) { $common++ } } $changing = $script:PdeLayers.Count -gt $common -or $allowed.Count -gt $common $before = if ($changing -and !$Quiet) { Get-PdeSnapshot } else { $null } if ($script:PdeLayers.Count -gt $common) { $message = if ($Force -and $allowed.Count) { 'reloading' } else { 'unloading' } Write-PdeLog $message -Quiet:$Quiet } while ($script:PdeLayers.Count -gt $common) { Remove-PdeLastLayer } for ($index = 0; $index -lt $allowed.Count; $index++) { $profile = $allowed[$index] if ($index -ge $common) { Write-PdeLog ('loading ' + (Format-PdePath $profile.Path)) -Quiet:$Quiet try { Add-PdeLayer $profile } catch { $profile.Status = 'Failed'; $profile.Error = $_.Exception.Message $script:PdeFailures[$profile.Path] = @{ Hash = $profile.Hash; Error = $profile.Error } Write-PdeNotice -Key ('error:' + $profile.Path + ':' + $profile.Hash) -Message $profile.Error -Quiet:$Quiet foreach ($child in $profiles | Select-Object -Skip ($index + 1)) { $child.Status = 'Blocked' } break } } $profile.Status = 'Loaded' } $script:PdeProfiles = $profiles if ($null -ne $before) { Write-PdeDiff $before (Get-PdeSnapshot) } } catch { throw (Format-PdeMessage $_.Exception.Message -Error) } finally { $script:PdeBusy = $false } } function Enable-DirEnv { <# .SYNOPSIS Enable automatic .ps.envrc loading before the existing prompt is displayed. #> [CmdletBinding()] param([string]$StatePath) if ($PSBoundParameters.ContainsKey('StatePath')) { $resolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($StatePath) if ($script:PdeEnabled -and !$script:PdePathComparer.Equals($resolved, $script:PdeStorePath)) { throw (Format-PdeMessage 'run Disable-DirEnv before changing StatePath' -Error) } $script:PdeStorePath = $resolved } if ($script:PdeEnabled) { Update-DirEnv; return } $script:PdeOriginalPrompt = (Get-PdeSnapshot).Function['prompt'] $original = if ($null -ne $script:PdeOriginalPrompt) { $script:PdeOriginalPrompt.ScriptBlock } else { { 'PS> ' } } $update = Get-Command Update-DirEnv -CommandType Function $log = Get-Command Write-PdeLog -CommandType Function $script:PdePrompt = { $previousSuccess = $? $hadExitCode = Test-Path Variable:\global:LASTEXITCODE $previousExitCode = if ($hadExitCode) { $global:LASTEXITCODE } else { $null } try { & $update } catch { & $log $_.Exception.Message -Error } finally { if ($hadExitCode) { $global:LASTEXITCODE = $previousExitCode } else { Remove-Variable -Scope Global -Name LASTEXITCODE -ErrorAction SilentlyContinue } } # Ignore avoids adding a synthetic error to $Error while restoring the # status the original prompt expects to see on entry. if ($previousSuccess) { $null = 0 } else { Write-Error 'Napreth.PSDirEnv prompt status' -ErrorAction Ignore } & $original }.GetNewClosure() Set-Item Function:\global:prompt -Value $script:PdePrompt -Force $script:PdeEnabled = $true try { Update-DirEnv } catch { Disable-DirEnv; throw } } function Disable-DirEnv { <# .SYNOPSIS Unload all managed overlays and restore the original prompt. #> [CmdletBinding()] param([switch]$Quiet) if ($script:PdeBusy) { throw (Format-PdeMessage 'cannot disable while a .ps.envrc is being evaluated' -Error) } $before = if ($script:PdeLayers.Count -and !$Quiet) { Get-PdeSnapshot } else { $null } if ($script:PdeLayers.Count) { Write-PdeLog 'unloading' -Quiet:$Quiet } while ($script:PdeLayers.Count) { Remove-PdeLastLayer } if ($null -ne $before) { Write-PdeDiff $before (Get-PdeSnapshot) } $current = Get-Item Function:\prompt -ErrorAction SilentlyContinue if ($null -ne $current -and [object]::ReferenceEquals($current.ScriptBlock, $script:PdePrompt)) { Set-PdeState Function prompt $script:PdeOriginalPrompt } $script:PdeEnabled = $false $script:PdeProfiles = @() $script:PdeFailures.Clear() $script:PdeNotices.Clear() $script:PdePrompt = $null $script:PdeOriginalPrompt = $null } function Approve-DirEnv { <# .SYNOPSIS Grants Napreth.PSDirEnv permission to load the given .ps.envrc file. #> [CmdletBinding(SupportsShouldProcess)] param([Parameter(Position = 0)][string]$Path = '.') if ($script:PdeBusy) { throw (Format-PdeMessage 'cannot change approvals while a .ps.envrc is being evaluated' -Error) } $file = Resolve-PdeProfilePath $Path $profile = Read-PdeProfile $file if ($PSCmdlet.ShouldProcess($file, 'Allow loading .ps.envrc')) { Write-PdeApproval $file $profile.Hash $script:PdeFailures.Remove($file) Update-DirEnv } } function Revoke-DirEnv { <# .SYNOPSIS Revokes the authorization of a given .ps.envrc file. #> [CmdletBinding(SupportsShouldProcess)] param([Parameter(Position = 0)][string]$Path = '.') if ($script:PdeBusy) { throw (Format-PdeMessage 'cannot change approvals while a .ps.envrc is being evaluated' -Error) } $file = Resolve-PdeProfilePath $Path if ($PSCmdlet.ShouldProcess($file, 'Revoke authorization of .ps.envrc')) { Write-PdeApproval $file $null $script:PdeFailures.Remove($file) Update-DirEnv } } function Get-DirEnv { <# .SYNOPSIS Show enabled state, active overlays and the last refresh results. #> [CmdletBinding()] param() [pscustomobject]@{ Enabled = $script:PdeEnabled Path = (Get-Location).Path StatePath = $script:PdeStorePath LoadedProfiles = @($script:PdeLayers | ForEach-Object Path) Profiles = @($script:PdeProfiles | Select-Object Path, Hash, Approved, Status, Error) } } $ExecutionContext.SessionState.Module.OnRemove = { Disable-DirEnv } Export-ModuleMember -Function Enable-DirEnv, Disable-DirEnv, Approve-DirEnv, Revoke-DirEnv, Update-DirEnv, Get-DirEnv |