< Summary

Information
Class: ImportDotEnv/ImportDotEnv
Assembly: ImportDotEnv
File(s): .\ImportDotEnv.psm1
Line coverage
87%
Covered lines: 315
Uncovered lines: 43
Coverable lines: 358
Total lines: 782
Line coverage: 87.9%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

.\ImportDotEnv.psm1

#LineLine coverage
 1# DotEnv.psm1
 2
 3# Requires -Version 5.1
 4
 5using namespace System.IO
 6using namespace System.Management.Automation
 7
 18$script:trueOriginalEnvironmentVariables = @{} # Stores { VarName = OriginalValueOrNull } - a persistent record of pre-m
 19$script:previousEnvFiles = @()
 110$script:previousWorkingDirectory = $PWD.Path
 111$script:e = [char]27
 112$script:itemiserA = [char]0x2022
 113$script:itemiser = [char]0x21B3
 114$script:boldOn = "$($script:e)[1m"
 115$script:boldOff = "$($script:e)[0m" # Resets all attributes (color, bold, underline etc.)
 16
 17# $DebugPreference = 'Continue'
 18
 19function Get-RelativePath {
 20  [CmdletBinding()]
 21  param(
 22    [Parameter(Mandatory)]
 23    [string]$Path,
 24
 25    [Parameter(Mandatory)]
 26    [string]$BasePath
 27  )
 28
 29  try {
 130    $absTarget = [System.IO.Path]::GetFullPath($Path)
 131    $absBase = [System.IO.Path]::GetFullPath($BasePath)
 32
 133    if ($absTarget.Equals($absBase, [System.StringComparison]::OrdinalIgnoreCase)) {
 034        return "."
 35    }
 36
 37    # Ensure BasePath for Uri ends with a directory separator.
 138    $uriBaseNormalized = $absBase
 139    if (-not $uriBaseNormalized.EndsWith([System.IO.Path]::DirectorySeparatorChar)) {
 140        $uriBaseNormalized += [System.IO.Path]::DirectorySeparatorChar
 41    }
 142    $baseUri = [System.Uri]::new($uriBaseNormalized)
 143    $targetUri = [System.Uri]::new($absTarget)
 44
 145    $relativeUri = $baseUri.MakeRelativeUri($targetUri)
 146    $relativePath = [System.Uri]::UnescapeDataString($relativeUri.ToString())
 47
 148    return $relativePath.Replace('/', [System.IO.Path]::DirectorySeparatorChar)
 49  }
 50  catch {
 051    Write-Warning "Get-RelativePath: Error calculating relative path for Target '$Path' from Base '$BasePath'. Error: $(
 052    return $Path
 53  }
 54}
 55
 56# Cannot be local as this is mocked in Import-DotEnv tests
 57function Get-EnvFilesUpstream {
 58  [CmdletBinding()]
 59  param([string]$Directory = ".")
 60
 61  try {
 162    $resolvedPath = Convert-Path -Path $Directory -ErrorAction Stop
 63  }
 64  catch {
 065    Write-Warning "Get-EnvFilesUpstream: Error resolving path '$Directory'. Error: $($_.Exception.Message). Defaulting t
 066    $resolvedPath = $PWD.Path
 67    # Removed unused variable assignment for $currentDirNormalized
 68  }
 69
 170  $envFiles = [System.Collections.Generic.List[string]]::new()
 171  $currentSearchDir = $resolvedPath
 72
 173  while ($currentSearchDir) {
 174    $envPath = Join-Path $currentSearchDir ".env"
 175    if (Test-Path -LiteralPath $envPath -PathType Leaf) {
 176      $envFiles.Add($envPath)
 77    }
 178    $parentDir = Split-Path -Path $currentSearchDir -Parent
 179    if ($parentDir -eq $currentSearchDir -or [string]::IsNullOrEmpty($parentDir)) { break }
 180    $currentSearchDir = $parentDir
 81  }
 82
 183  if ($envFiles.Count -gt 0) {
 184    $envFiles.Reverse()
 85  }
 186  return [string[]]$envFiles
 87}
 88
 89function Format-EnvFilePath {
 90  [CmdletBinding()]
 91  param(
 92    [Parameter(Mandatory)]
 93    [string]$Path,
 94
 95    [Parameter(Mandatory)]
 96    [string]$BasePath
 97  )
 98
 199  $relativePath = Get-RelativePath -Path $Path -BasePath $BasePath
 1100  $corePath = Split-Path -Path $relativePath -Parent
 101
 1102  if (-not [string]::IsNullOrEmpty($corePath)) {
 1103    $boldCore = "${script:e}[1m${corePath}${script:e}[22m"
 1104    $relativePath = $relativePath.Replace($corePath, $boldCore)
 105  }
 106
 1107  return $relativePath
 108}
 109
 110function Format-VarHyperlink {
 111    param(
 112        [string]$VarName,
 113        [string]$FilePath,
 114        [int]$LineNumber
 115    )
 116    # Ensure FilePath is absolute for the hyperlink
 1117    $absFilePath = try { Resolve-Path -LiteralPath $FilePath -ErrorAction Stop } catch { $FilePath }
 1118    $fileUrl = "vscode://file/$($absFilePath):${LineNumber}"
 1119    return "$script:e]8;;$fileUrl$script:e\$VarName$script:e]8;;$script:e\"
 120}
 121
 122# --- Helper function to get effective environment variables from a list of .env files ---
 123function Get-EnvVarsFromFiles {
 124    param(
 125        [string[]]$Files,
 126        [string]$BasePath # BasePath is for context, not directly used in var aggregation here
 127    )
 128
 129  function Read-EnvFile {
 130      param([string]$FilePath)
 1131      $vars = @{}
 1132      if (-not ([System.IO.File]::Exists($FilePath))) {
 1133          Write-Debug "Parse-EnvFile: File '$FilePath' does not exist."
 1134          return $vars
 135      }
 136      try {
 1137          $lines = [System.IO.File]::ReadLines($FilePath)
 138      } catch {
 0139          Write-Warning "Parse-EnvFile: Error reading file '$FilePath'. Error: $($_.Exception.Message)"
 0140          return $vars
 141      }
 1142      $lineNumber = 0
 1143      foreach ($line in $lines) {
 1144          $lineNumber++
 1145          if ([string]::IsNullOrWhiteSpace($line)) { continue }
 1146          $trimmed = $line.TrimStart()
 1147          if ($trimmed.StartsWith('#')) { continue }
 1148          $split = $line.Split('=', 2)
 1149          if ($split.Count -eq 2) {
 1150              $varName = $split[0].Trim()
 1151              $varValue = $split[1].Trim()
 1152              $vars[$varName] = @{ Value = $varValue; Line = $lineNumber; SourceFile = $FilePath }
 153          }
 154      }
 1155      return $vars
 156  }
 157
 1158    if ($Files.Count -eq 0) {
 1159        return @{}
 160    }
 161
 1162    if ($Files.Count -eq 1) {
 163        # Fast path for a single file. Parse-EnvFile returns the rich structure.
 1164        return Read-EnvFile -FilePath $Files[0]
 165    }
 166
 167    # For multiple files, use RunspacePool for parallel parsing.
 1168    $finalEffectiveVars = @{}
 1169    $parsedResults = New-Object "object[]" $Files.Count # To store results in order
 170
 171    # Define the script that will be run in each runspace.
 172    # It includes a minimal Parse-EnvFile definition to ensure it's available and self-contained.
 1173    $scriptBlockText = @'
 174param([string]$PathToParse)
 175
 176# Minimal Parse-EnvFile definition for use in isolated runspaces
 177function Parse-EnvFileInRunspace {
 178    param([string]$LocalFilePath)
 179    $localVars = @{} # PowerShell hashtable literal is fine here, it's a PS runspace
 180    # Directly use System.IO.File for existence and reading to minimize dependencies
 181    if (-not ([System.IO.File]::Exists($LocalFilePath))) {
 182        return $localVars
 183    }
 184    try {
 185        $fileLines = [System.IO.File]::ReadLines($LocalFilePath)
 186    } catch {
 187        # Silently return empty on read error in this isolated context
 188        return $localVars
 189    }
 190    $lineNum = 0
 191    foreach ($txtLine in $fileLines) {
 192        $lineNum++
 193        if ([string]::IsNullOrWhiteSpace($txtLine)) { continue }
 194        $trimmedTxtLine = $txtLine.TrimStart()
 195        if ($trimmedTxtLine.StartsWith('#')) { continue }
 196        $parts = $txtLine.Split('=', 2)
 197        if ($parts.Count -eq 2) {
 198            $name = $parts[0].Trim()
 199            $val = $parts[1].Trim()
 200            # This structure needs to match what the rest of the module expects
 201            $localVars[$name] = @{ Value = $val; Line = $lineNum; SourceFile = $LocalFilePath }
 202        }
 203    }
 204    return $localVars
 205}
 206
 207Parse-EnvFileInRunspace -LocalFilePath $PathToParse
 208'@
 209
 210    # Determine a reasonable number of runspaces. Cap at 8 to avoid excessive resource use.
 211    # Fix: [Math]::Min takes only two arguments. Nest calls for three values.
 1212    $maxRunspaces = [Math]::Min(8, [Math]::Min($Files.Count, ([System.Environment]::ProcessorCount * 2)))
 1213    $minRunspaces = 1
 214
 1215    $iss = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault2()
 216    # CreateDefault2 is generally good for providing access to common .NET types like System.IO.File
 217
 1218    $runspacePool = $null
 1219    $psInstanceTrackers = [System.Collections.Generic.List[object]]::new()
 220
 221    try {
 1222        $runspacePool = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspacePool($minRunspaces, $max
 1223        $runspacePool.Open()
 224
 1225        for ($i = 0; $i -lt $Files.Count; $i++) {
 1226            $fileToParse = $Files[$i]
 1227            $ps = [PowerShell]::Create()
 1228            $ps.RunspacePool = $runspacePool
 1229            $null = $ps.AddScript($scriptBlockText).AddArgument($fileToParse)
 230
 1231            $asyncResult = $ps.BeginInvoke()
 1232            $psInstanceTrackers.Add([PSCustomObject]@{
 1233                PowerShell    = $ps
 1234                AsyncResult   = $asyncResult
 1235                OriginalIndex = $i
 1236                FilePath      = $fileToParse # For logging/debugging
 237            })
 238        }
 239
 240        # Wait for all to complete and collect results
 1241        foreach ($tracker in $psInstanceTrackers) {
 242            try {
 1243                $outputCollection = $tracker.PowerShell.EndInvoke($tracker.AsyncResult)
 244
 1245                if ($tracker.PowerShell.Streams.Error.Count -gt 0) {
 0246                    foreach($err in $tracker.PowerShell.Streams.Error){
 0247                        Write-Warning "Error parsing file '$($tracker.FilePath)' in parallel: $($err.ToString())"
 248                    }
 0249                    $parsedResults[$tracker.OriginalIndex] = @{}
 1250                } elseif ($null -ne $outputCollection -and $outputCollection.Count -eq 1) {
 1251                    $singleOutput = $outputCollection[0]
 1252                    if ($singleOutput -is [System.Collections.IDictionary]) { # Directly a hashtable
 1253                        $parsedResults[$tracker.OriginalIndex] = $singleOutput
 0254                    } elseif ($singleOutput -is [System.Management.Automation.PSObject] -and $singleOutput.BaseObject -i
 0255                        $parsedResults[$tracker.OriginalIndex] = $singleOutput.BaseObject
 256                    } else {
 0257                        Write-Warning "Unexpected output type from parallel parsing of '$($tracker.FilePath)'. Type: $($
 0258                        $parsedResults[$tracker.OriginalIndex] = @{}
 259                    }
 260                } else {
 0261                    Write-Warning "No output or multiple outputs from parallel parsing of '$($tracker.FilePath)'. Output
 0262                    $parsedResults[$tracker.OriginalIndex] = @{}
 263                }
 264            } catch {
 0265                 Write-Warning "Exception during EndInvoke for file '$($tracker.FilePath)': $($_.Exception.Message)"
 0266                 $parsedResults[$tracker.OriginalIndex] = @{} # Store empty on exception
 267            }
 268        }
 269    }
 270    finally {
 1271        foreach ($tracker in $psInstanceTrackers) {
 1272            if ($tracker.PowerShell) {
 1273                $tracker.PowerShell.Dispose()
 274            }
 275        }
 1276        if ($runspacePool) {
 1277            $runspacePool.Close()
 1278            $runspacePool.Dispose()
 279        }
 280    }
 281
 282    # Sequentially merge the parsed results to ensure correct precedence.
 1283    foreach ($fileScopedVarsHashtable in $parsedResults) {
 1284        if ($null -eq $fileScopedVarsHashtable) { continue } # Skip if null (e.g. error during parsing)
 1285        foreach ($varNameKey in $fileScopedVarsHashtable.Keys) {
 1286            $finalEffectiveVars[$varNameKey] = $fileScopedVarsHashtable[$varNameKey]
 287        }
 288    }
 1289    return $finalEffectiveVars
 290}
 291
 292function Import-DotEnv {
 293  [CmdletBinding(DefaultParameterSetName = 'Load', HelpUri = 'https://github.com/CosmicDNA/ImportDotEnv#readme')]
 294  param(
 295    [Parameter(ParameterSetName = 'Load', Position = 0, ValueFromPipelineByPropertyName = $true)]
 296    [string]$Path,
 297
 298    [Parameter(ParameterSetName = 'Unload')]
 299    [switch]$Unload,
 300
 301    [Parameter(ParameterSetName = 'Help')]
 302    [switch]$Help,
 303
 304    [Parameter(ParameterSetName = 'List')]
 305    [switch]$List
 306  )
 307
 308  # --- Helper: Parse a single .env line into [name, value] or $null ---
 309  function Convert-EnvLine {
 310    param([string]$Line)
 1311    if ([string]::IsNullOrWhiteSpace($Line)) { return $null }
 1312    $trimmed = $Line.TrimStart()
 1313    if ($trimmed.StartsWith('#')) { return $null }
 1314    $split = $Line.Split('=', 2)
 1315    if ($split.Count -eq 2) {
 1316      return @($split[0].Trim(), $split[1].Trim())
 317    }
 0318    return $null
 319  }
 320
 321  function Get-VarsToRestoreByFileMap {
 322    param(
 323      [string[]]$Files,
 324      [string[]]$VarsToRestore
 325    )
 326
 327    function Get-EnvVarNamesFromFile {
 328      param([string]$FilePath)
 1329      if (-not (Test-Path -LiteralPath $FilePath -PathType Leaf)) { return @() }
 330      try {
 1331        return [System.IO.File]::ReadLines($FilePath) | ForEach-Object {
 1332          $parsed = Convert-EnvLine $_
 1333          if ($null -ne $parsed) { $parsed[0] }
 1334        } | Where-Object { $_ }
 335      } catch {
 0336        Write-Warning "Get-EnvVarNamesFromFile: Error reading file '$FilePath'. Skipping. Error: $($_.Exception.Message)
 0337        return @()
 338      }
 339    }
 340
 1341    $varsToUnsetByFileMap = @{}
 1342    foreach ($fileToScan in $Files) {
 1343      foreach ($parsedVarName in Get-EnvVarNamesFromFile -FilePath $fileToScan) {
 1344        if ($VarsToRestore -contains $parsedVarName) {
 1345          if (-not $varsToUnsetByFileMap.ContainsKey($fileToScan)) { $varsToUnsetByFileMap[$fileToScan] = [System.Collec
 1346          $varsToUnsetByFileMap[$fileToScan].Add($parsedVarName)
 347        }
 348      }
 349    }
 1350    return $varsToUnsetByFileMap
 351  }
 352
 1353  if ($PSCmdlet.ParameterSetName -eq 'Unload') {
 1354    Write-Debug "MODULE Import-DotEnv: Called with -Unload switch."
 1355    $varsFromLastLoad = Get-EnvVarsFromFiles -Files $script:previousEnvFiles -BasePath $script:previousWorkingDirectory
 356
 1357    if ($varsFromLastLoad.Count -gt 0) {
 1358      Write-Host "`nUnloading active .env configuration(s)..." -ForegroundColor Yellow
 359
 1360      $allVarsToRestore = $varsFromLastLoad.Keys
 1361      $varsToRestoreByFileMap = Get-VarsToRestoreByFileMap -Files $script:previousEnvFiles -VarsToRestore $allVarsToRest
 362
 1363      $varsCoveredByFileMap = $varsToRestoreByFileMap.Values | ForEach-Object { $_ } | Sort-Object -Unique
 1364      $varsToRestoreNoFileAssociation = $allVarsToRestore | Where-Object { $varsCoveredByFileMap -notcontains $_ }
 365
 1366      Restore-EnvVars -VarsToRestoreByFileMap $varsToRestoreByFileMap -VarNames $varsToRestoreNoFileAssociation -TrueOri
 367
 1368      $script:previousEnvFiles = @()
 1369      $script:previousWorkingDirectory = "STATE_AFTER_EXPLICIT_UNLOAD"
 1370      Write-Host "Environment restored. Module state reset." -ForegroundColor Green
 371    }
 372    return
 373  }
 374
 1375  if ($PSCmdlet.ParameterSetName -eq 'Help' -or $Help) {
 0376    Write-Host @"
 377
 378`e[1mImport-DotEnv Module Help`e[0m
 379
 380This module allows for hierarchical loading and unloading of .env files.
 381It also provides integration with `Set-Location` (cd/sl) to automatically
 382manage environment variables as you navigate directories.
 383
 384`e[1mUsage:`e[0m
 385
 386  `e[1mImport-DotEnv`e[0m [-Path <string>]
 387    Loads .env files from the specified path (or current directory if no path given)
 388    and its parent directories. Variables from deeper .env files take precedence.
 389    Automatically unloads variables from previously loaded .env files if they are
 390    no longer applicable or have changed.
 391
 392  `e[1mImport-DotEnv -Unload`e[0m
 393    Unloads all variables set by the module and resets its internal state.
 394
 395  `e[1mImport-DotEnv -List`e[0m
 396    Lists currently active variables and the .env files defining them.
 397
 398  `e[1mImport-DotEnv -Help`e[0m
 399    Displays this help message.
 400
 401For `Set-Location` integration, use `Enable-ImportDotEnvCdIntegration` and `Disable-ImportDotEnvCdIntegration`.
 402"@
 403    return
 404  }
 405
 1406  if ($PSCmdlet.ParameterSetName -eq 'List') {
 1407    Write-Debug "MODULE Import-DotEnv: Called with -List switch."
 1408    if (-not $script:previousEnvFiles -or $script:previousEnvFiles.Count -eq 0 -or $script:previousWorkingDirectory -eq 
 1409      Write-Host "No .env configuration is currently active or managed by ImportDotEnv." -ForegroundColor Magenta
 410      return
 411    }
 1412    $effectiveVars = Get-EnvVarsFromFiles -Files $script:previousEnvFiles -BasePath $script:previousWorkingDirectory
 413    function Get-VarToFilesMap($files) {
 1414      $map = @{}
 1415      foreach ($file in $files) {
 1416        if (Test-Path -LiteralPath $file -PathType Leaf) {
 1417          foreach ($line in [System.IO.File]::ReadLines($file)) {
 1418            $parsed = Convert-EnvLine $line
 1419            if ($parsed) {
 1420              $var = $parsed[0]
 1421              if (-not $map[$var]) { $map[$var] = @() }
 1422              $map[$var] += $file
 423            }
 424          }
 425        }
 426      }
 1427      $map
 428    }
 1429    $varToFiles = Get-VarToFilesMap $script:previousEnvFiles
 1430    $outputObjects = $effectiveVars.Keys | Sort-Object | ForEach-Object {
 1431      $var = $_
 1432      $varPlainName = $var # Store plain name for calculations
 1433      $effectiveVarDetail = $effectiveVars[$var] # Get details of the effective variable (SourceFile, Line)
 1434      $hyperlinkedName = Format-VarHyperlink -VarName $varPlainName -FilePath $effectiveVarDetail.SourceFile -LineNumber
 435
 436      # For 'Defined In', list all files where the variable name appears
 1437      $definingFilesPaths = $varToFiles[$var] # This is an array of file paths from Get-VarToFilesMap
 1438      $definedInDisplay = ($definingFilesPaths | ForEach-Object { "  $(Get-RelativePath -Path $_ -BasePath $PWD.Path)" }
 439
 1440      [PSCustomObject]@{
 1441        NameForOutput    = $hyperlinkedName  # Always the hyperlinked version
 1442        NamePlainForCalc = $varPlainName     # Always the plain version, for calculations
 1443        'Defined In'     = $definedInDisplay
 444      }
 445    }
 1446    if ($outputObjects) {
 1447      if ($PSVersionTable.PSVersion.Major -ge 7) {
 448        # For PS7+, use NameForOutput (which has hyperlink), Format-Table handles ANSI well.
 449        # Ensure the column header is "Name".
 1450        $outputObjects | Format-Table -Property @{Expression={$_.NameForOutput}; Label="Name"}, 'Defined In' -AutoSize
 451      } else {
 452        # PS5.1: Manual formatting to try and preserve hyperlinks while maintaining table structure.
 453        # This works best in terminals that understand ANSI hyperlinks (like Windows Terminal running PS5.1).
 454        # In older conhost.exe, ANSI codes might print literally.
 1455        $maxPlainNameLength = 0
 1456        $nameLengths = $outputObjects | ForEach-Object { $_.NamePlainForCalc.Length }
 1457        if ($nameLengths) {
 1458            $maxPlainNameLength = ($nameLengths | Measure-Object -Maximum).Maximum
 459        }
 460        # Ensure $nameColPaddedWidth is a clean integer for use in format strings.
 1461        $nameColPaddedWidth = [int]([Math]::Max("Name".Length, $maxPlainNameLength))
 462
 1463        $nameHeaderTextPlain = "Name"
 1464        $definedInHeaderTextPlain = "Defined In"
 465
 1466        $nameHeaderFormatted = "$($script:boldOn)${nameHeaderTextPlain}$($script:boldOff)"
 1467        $definedInHeaderFormatted = "$($script:boldOn)${definedInHeaderTextPlain}$($script:boldOff)"
 468
 1469        Write-Host ""
 470        # --- Print Header Titles ---
 1471        Write-Host -NoNewline $nameHeaderFormatted -ForegroundColor Green
 472        # Calculate padding based on the plain text length of the "Name" header
 1473        $paddingForNameHeader = [Math]::Max(0, $nameColPaddedWidth - $nameHeaderTextPlain.Length)
 1474        Write-Host -NoNewline (" " * $paddingForNameHeader)
 1475        Write-Host -NoNewline "  " # Column separator
 1476        Write-Host $definedInHeaderFormatted -ForegroundColor Green
 477
 478        # --- Print Header Underlines ---
 1479        $nameUnderline = "-" * $nameColPaddedWidth # Underline spans the full calculated width of the first column
 1480        $definedInUnderline = "-" * $definedInHeaderTextPlain.Length # Underline matches the visible text of "Defined In
 1481        Write-Host -NoNewline $nameUnderline -ForegroundColor Green
 1482        Write-Host -NoNewline "  " # Column separator
 1483        Write-Host $definedInUnderline -ForegroundColor Green
 484
 1485        foreach ($obj in $outputObjects) {
 1486            $nameToPrint = $obj.NameForOutput # This is the hyperlink string
 1487            $plainNameActualLength = $obj.NamePlainForCalc.Length # Calculate actual length of the plain name
 1488            $definedInLines = $obj.'Defined In' -split [Environment]::NewLine
 489
 1490            Write-Host -NoNewline $nameToPrint
 1491            $spacesNeededAfterName = [Math]::Max(0, $nameColPaddedWidth - $plainNameActualLength) # Use calculated plain
 1492            Write-Host -NoNewline (" " * $spacesNeededAfterName)
 1493            Write-Host -NoNewline "  " # Column separator
 1494            Write-Host $definedInLines[0] # First line of "Defined In"
 495            # Subsequent lines of "Defined In", correctly indented
 1496            for ($j = 1; $j -lt $definedInLines.Length; $j++) {
 1497                Write-Host (" " * ($nameColPaddedWidth + 2)) $definedInLines[$j] # Indent under "Defined In"
 498            }
 499        }
 1500        Write-Host ""
 501      }
 502    } else {
 0503      Write-Host "No effective variables found in the active configuration." -ForegroundColor Yellow
 504    }
 505    return
 506  }
 507
 508  # --- Load Parameter Set Logic (Default) ---
 1509  Write-Debug "MODULE Import-DotEnv: Called with Path '$Path' (Load set). Current PWD: $($PWD.Path)"
 1510  if ($PSCmdlet.ParameterSetName -eq 'Load' -and (-not $PSBoundParameters.ContainsKey('Path'))) {
 0511    $Path = "."
 512  }
 513  try {
 1514    $resolvedPath = Convert-Path -Path $Path -ErrorAction Stop
 515  } catch {
 0516    $resolvedPath = $PWD.Path
 0517    Write-Warning "Import-DotEnv: The specified path '$Path' could not be resolved. Falling back to current directory: '
 0518    Write-Debug "MODULE Import-DotEnv: Path '$Path' resolved to PWD '$resolvedPath' due to error: $($_.Exception.Message
 519  }
 520
 1521  $currentEnvFiles = Get-EnvFilesUpstream -Directory $resolvedPath
 1522  Write-Debug "MODULE Import-DotEnv: Resolved path '$resolvedPath'. Found $($currentEnvFiles.Count) .env files upstream:
 1523  Write-Debug "MODULE Import-DotEnv: Previous files count: $($script:previousEnvFiles.Count) ('$($script:previousEnvFile
 524
 1525  $prevVars = Get-EnvVarsFromFiles -Files $script:previousEnvFiles -BasePath $script:previousWorkingDirectory
 1526  $currVars = Get-EnvVarsFromFiles -Files $currentEnvFiles -BasePath $resolvedPath
 527
 528  # --- Unload Phase: Unset variables that were in prevVars but not in currVars, or if their value changed ---
 1529  $varsToUnsetOrRestore = @()
 1530  foreach ($varNameKey in $prevVars.Keys) {
 1531    if (-not $currVars.ContainsKey($varNameKey) -or $currVars[$varNameKey].Value -ne $prevVars[$varNameKey].Value) {
 1532      $varsToUnsetOrRestore += $varNameKey
 533    }
 534  }
 535
 1536  if ($varsToUnsetOrRestore.Count -gt 0) {
 1537    $varsToRestoreByFileMap = Get-VarsToRestoreByFileMap -Files $script:previousEnvFiles -VarsToRestore $varsToUnsetOrRe
 1538    $varsCoveredByFileMap = $varsToRestoreByFileMap.Values | ForEach-Object { $_ } | Sort-Object -Unique
 1539    $varsToRestoreNoFileAssociation = $varsToUnsetOrRestore | Where-Object { $varsCoveredByFileMap -notcontains $_ }
 1540    Restore-EnvVars -VarsToRestoreByFileMap $varsToRestoreByFileMap -VarNames $varsToRestoreNoFileAssociation -TrueOrigi
 541  }
 542
 543  # --- Load Phase ---
 1544  if ($currentEnvFiles.Count -gt 0) {
 1545    foreach ($varNameKey in $currVars.Keys) {
 1546      if (-not $script:trueOriginalEnvironmentVariables.ContainsKey($varNameKey)) {
 1547        $currentEnvValue = [Environment]::GetEnvironmentVariable($varNameKey, 'Process')
 1548        if (-not (Test-Path "Env:\$varNameKey")) {
 1549          $script:trueOriginalEnvironmentVariables[$varNameKey] = $null
 550        } else {
 1551          $script:trueOriginalEnvironmentVariables[$varNameKey] = $currentEnvValue
 552        }
 553      }
 554    }
 555
 1556    $varsToReportAsSetOrChanged = [System.Collections.Generic.List[PSCustomObject]]::new() # Changed to PSCustomObject
 1557    foreach ($varNameKey in $currVars.Keys) {
 1558      $desiredVarInfo = $currVars[$varNameKey]
 1559      $desiredValue = $desiredVarInfo.Value
 1560      $currentValue = [Environment]::GetEnvironmentVariable($varNameKey, 'Process')
 561      # Fix: Correctly set empty string as value, not as $null (which unsets)
 1562      if ($currentValue -ne $desiredValue) {
 1563        if ($null -eq $desiredValue) {
 0564          [Environment]::SetEnvironmentVariable($varNameKey, $null)
 565        } else {
 1566          [Environment]::SetEnvironmentVariable($varNameKey, $desiredValue)
 567        }
 568      }
 1569      $isNewToSession = (-not $prevVars.ContainsKey($varNameKey))
 1570      $hasValueChanged = $false
 1571      if (-not $isNewToSession -and $prevVars[$varNameKey].Value -ne $desiredValue) {
 1572          $hasValueChanged = $true
 573      }
 1574      Write-Verbose "Var: '$varNameKey', IsNew: $isNewToSession, HasChanged: $hasValueChanged"
 1575      if (-not $isNewToSession) {
 1576        Write-Verbose "  PrevValue: '$($prevVars[$varNameKey].Value)', DesiredValue: '$desiredValue'"
 577      }
 578
 1579      if ($isNewToSession -or $hasValueChanged) {
 1580        $varsToReportAsSetOrChanged.Add([PSCustomObject]@{ # Changed to PSCustomObject
 1581            Name       = $varNameKey
 1582            Line       = $desiredVarInfo.Line
 1583            SourceFile = $desiredVarInfo.SourceFile
 584        })
 585      }
 586    }
 587
 1588    if ($varsToReportAsSetOrChanged.Count -gt 0) {
 1589      $groupedBySourceFile = $varsToReportAsSetOrChanged | Group-Object -Property SourceFile
 1590      foreach ($fileGroup in $groupedBySourceFile) {
 1591        $sourceFilePath = $fileGroup.Name # This is "" in PS5.1 if SourceFile was $null, and $null in PS7+
 592
 593        # If SourceFile was $null (or missing), its group name might be $null or ""
 594        # Skip processing for such groups as they don't represent a valid file path.
 1595        if ([string]::IsNullOrEmpty($sourceFilePath)) {
 0596          Write-Debug "Skipping report for variables with no valid SourceFile (group name was '$sourceFilePath')."
 597          continue
 598        }
 599
 1600        $formattedPath = Format-EnvFilePath -Path $sourceFilePath -BasePath $script:previousWorkingDirectory # Now $sour
 1601        Write-Host "$script:itemiserA Processing .env file ${formattedPath}:" -ForegroundColor Cyan
 1602        foreach ($varDetail in $fileGroup.Group) {
 1603          $hyperlink = Format-VarHyperlink -VarName $varDetail.Name -FilePath $varDetail.SourceFile -LineNumber $varDeta
 1604          Write-Host "  $script:itemiser Setting environment variable: " -NoNewline
 1605          Write-Host $hyperlink -ForegroundColor Green -NoNewline
 1606          Write-Host " (from line $($varDetail.Line))"
 607        }
 608      }
 609    }
 610  }
 611
 1612  $script:previousEnvFiles = $currentEnvFiles
 1613  $script:previousWorkingDirectory = $resolvedPath
 614}
 615
 616# This function will be the wrapper for Set-Location
 617function Invoke-ImportDotEnvSetLocationWrapper {
 618  [CmdletBinding(DefaultParameterSetName = 'Path', SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
 619  param(
 620    [Parameter(ParameterSetName = 'Path', Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
 621    [string]$Path,
 622    [Parameter(ParameterSetName = 'LiteralPath', Mandatory, ValueFromPipelineByPropertyName)]
 623    [Alias('PSPath')]
 624    [string]$LiteralPath,
 625    [Parameter()]
 626    [switch]$PassThru,
 627    [Parameter()]
 628    [string]$StackName
 629  )
 630
 1631  $slArgs = @{}
 1632  if ($PSCmdlet.ParameterSetName -eq 'Path') {
 1633    if ($PSBoundParameters.ContainsKey('Path')) { $slArgs.Path = $Path }
 0634  } elseif ($PSCmdlet.ParameterSetName -eq 'LiteralPath') {
 0635    $slArgs.LiteralPath = $LiteralPath
 636  }
 1637  if ($PSBoundParameters.ContainsKey('PassThru')) { $slArgs.PassThru = $PassThru }
 1638  if ($PSBoundParameters.ContainsKey('StackName')) { $slArgs.StackName = $StackName }
 639
 1640  $CommonParameters = @('Verbose', 'Debug', 'ErrorAction', 'ErrorVariable', 'WarningAction', 'WarningVariable',
 641    'OutBuffer', 'OutVariable', 'PipelineVariable', 'InformationAction', 'InformationVariable', 'WhatIf', 'Confirm')
 1642  foreach ($commonParam in $CommonParameters) {
 1643    if ($PSBoundParameters.ContainsKey($commonParam)) {
 0644      $slArgs[$commonParam] = $PSBoundParameters[$commonParam]
 645    }
 646  }
 647
 1648  Microsoft.PowerShell.Management\Set-Location @slArgs
 1649  Import-DotEnv -Path $PWD.Path
 650}
 651
 652function Enable-ImportDotEnvCdIntegration {
 653  [CmdletBinding()]
 654  param()
 1655  $currentModuleForEnable = $MyInvocation.MyCommand.Module
 1656  if (-not $currentModuleForEnable) {
 0657    Write-Error "Enable-ImportDotEnvCdIntegration: Module context not found." -ErrorAction Stop
 658  }
 1659  if (-not $currentModuleForEnable.ExportedCommands.ContainsKey('Invoke-ImportDotEnvSetLocationWrapper')) {
 0660    Write-Error "Enable-ImportDotEnvCdIntegration: Required wrapper 'Invoke-ImportDotEnvSetLocationWrapper' is not expor
 661  }
 662
 1663  Write-Host "Enabling ImportDotEnv integration for 'Set-Location', 'cd', and 'sl' commands..." -ForegroundColor Yellow
 1664  $wrapperFunctionFullName = "$($currentModuleForEnable.Name)\Invoke-ImportDotEnvSetLocationWrapper"
 1665  $existingSetLocation = Get-Command Set-Location -ErrorAction SilentlyContinue
 1666  if ($existingSetLocation -and $existingSetLocation.CommandType -eq [System.Management.Automation.CommandTypes]::Alias)
 0667    if (Get-Alias -Name Set-Location -ErrorAction SilentlyContinue) {
 0668      Remove-Item -Path Alias:\Set-Location -Force -ErrorAction SilentlyContinue
 669    }
 670  }
 1671  Set-Alias -Name Set-Location -Value $wrapperFunctionFullName -Scope Global -Force -Option ReadOnly,AllScope
 1672  Import-DotEnv -Path $PWD.Path
 1673  Write-Host "ImportDotEnv 'Set-Location', 'cd', 'sl' integration enabled!" -ForegroundColor Green
 674}
 675
 676function Disable-ImportDotEnvCdIntegration {
 677  [CmdletBinding()]
 678  param()
 1679  Write-Host "Disabling ImportDotEnv integration for 'Set-Location', 'cd', and 'sl'..." -ForegroundColor Yellow
 1680  $currentModuleName = $MyInvocation.MyCommand.Module.Name
 1681  if (-not $currentModuleName) {
 0682    Write-Warning "Disable-ImportDotEnvCdIntegration: Could not determine module name. Assuming 'ImportDotEnv'."
 0683    $currentModuleName = "ImportDotEnv"
 684  }
 1685  $wrapperFunctionFullName = "$currentModuleName\Invoke-ImportDotEnvSetLocationWrapper"
 1686  $proxiesRemoved = $false
 687
 1688  $slCmdInfo = Get-Command "Set-Location" -ErrorAction SilentlyContinue
 1689  if ($slCmdInfo -and $slCmdInfo.CommandType -eq 'Alias' -and $slCmdInfo.Definition -eq $wrapperFunctionFullName) {
 1690    Remove-Item -Path Alias:\Set-Location -Force -ErrorAction SilentlyContinue
 1691    $proxiesRemoved = $true
 692  }
 693
 1694  Remove-Item -Path Alias:\Set-Location -Force -ErrorAction SilentlyContinue
 1695  Remove-Item "Function:\Global:Set-Location" -Force -ErrorAction SilentlyContinue
 696
 1697  $finalSetLocation = Get-Command "Set-Location" -ErrorAction SilentlyContinue
 1698  if ($null -eq $finalSetLocation -or $finalSetLocation.Source -ne "Microsoft.PowerShell.Management" -or $finalSetLocati
 0699    Write-Warning "Disable-ImportDotEnvCdIntegration: 'Set-Location' may not be correctly restored to the original cmdle
 700  }
 701
 1702  if ($proxiesRemoved) {
 1703    Write-Host "ImportDotEnv 'Set-Location' integration disabled, default command behavior restored." -ForegroundColor M
 704  } else {
 1705    Write-Host "ImportDotEnv 'Set-Location' integration was not active or already disabled." -ForegroundColor Magenta
 706  }
 1707  Write-Host "Active .env variables (if any) remain loaded. Use 'Import-DotEnv -Unload' to unload them." -ForegroundColo
 708}
 709
 1710Export-ModuleMember -Function Import-DotEnv,
 711Enable-ImportDotEnvCdIntegration,
 712Disable-ImportDotEnvCdIntegration,
 713Invoke-ImportDotEnvSetLocationWrapper
 714
 715function Restore-EnvVars {
 716  param(
 717    [hashtable]$VarsToRestoreByFileMap = $null,
 718    [string[]]$VarNames = $null,
 719    [hashtable]$TrueOriginalEnvironmentVariables,
 720    [string]$BasePath = $PWD.Path
 721  )
 1722  $restorationActions = @()
 1723  if ($VarsToRestoreByFileMap) {
 1724    foreach ($fileKey in $VarsToRestoreByFileMap.Keys) {
 1725      foreach ($var in $VarsToRestoreByFileMap[$fileKey]) {
 1726        $restorationActions += [PSCustomObject]@{ VarName = $var; SourceFile = $fileKey }
 727      }
 728    }
 729  }
 1730  if ($VarNames) {
 0731    $restorationActions += $VarNames | ForEach-Object { [PSCustomObject]@{ VarName = $_; SourceFile = $null } }
 732  }
 733
 734  function Restore-EnvVar {
 735    param(
 736      [string]$VarName,
 737      [hashtable]$TrueOriginalEnvironmentVariables,
 738      [string]$SourceFile = $null
 739    )
 740    function Set-OrUnset-EnvVar {
 741      param(
 742        [string]$Name,
 743        [object]$Value
 744      )
 1745      if ($null -eq $Value) {
 1746        [Environment]::SetEnvironmentVariable($Name, $null, 'Process')
 1747        Remove-Item "Env:\$Name" -Force -ErrorAction SilentlyContinue
 748      } else {
 1749        [Environment]::SetEnvironmentVariable($Name, $Value)
 750      }
 751    }
 752
 1753    $originalValue = $TrueOriginalEnvironmentVariables[$VarName]
 1754    Set-OrUnset-EnvVar -Name $VarName -Value $originalValue
 1755    $restoredActionText = if ($null -eq $originalValue) { "Unset" } else { "Restored" }
 1756    $hyperlink = if ($SourceFile) {
 1757      Format-VarHyperlink -VarName $VarName -FilePath $SourceFile -LineNumber 1
 758    } else {
 0759      $searchUrl = "vscode://search/search?query=$([System.Uri]::EscapeDataString($VarName))"
 0760      "$script:e]8;;$searchUrl$script:e\$VarName$script:e]8;;$script:e\"
 761    }
 1762    Write-Host "  $script:itemiser $restoredActionText environment variable: " -NoNewline
 763
 764    # Write-Host ($hyperlink -ne $null ? $hyperlink : $VarName) -ForegroundColor Yellow
 1765    $Output = if ($null -ne $hyperlink) { $hyperlink } else { $VarName }
 1766    Write-Host $Output -ForegroundColor Yellow
 767  }
 768
 1769  $restorationActions | Group-Object SourceFile | ForEach-Object {
 1770    $fileKey = $_.Name
 771
 772    # Write-Host ($fileKey ? "$script:itemiserA Restoring .env file $(Format-EnvFilePath -Path $fileKey -BasePath $BaseP
 1773    $envMessage = if ($fileKey) {
 1774        "$script:itemiserA Restoring .env file $(Format-EnvFilePath -Path $fileKey -BasePath $BasePath):"
 775    } else {
 0776        "Restoring environment variables not associated with any .env file:"
 777    }
 1778    Write-Host $envMessage -ForegroundColor Yellow
 779
 1780    $_.Group | ForEach-Object { Restore-EnvVar -VarName $_.VarName -TrueOriginalEnvironmentVariables $TrueOriginalEnviro
 781  }
 782}