posh-vsdev.psm1

if (Get-Module posh-vsdev) { return; }

# Simplifies access to HashSet<string>
class Set : System.Collections.Generic.HashSet[string] {
    Set() { }
    Set([string[]] $Data) {
        foreach($local:Item in $Data) {
            $this.Add($local:Item);
        }
    }
}

# Encapsulates environment variables and their values
class Environment : System.Collections.Generic.Dictionary[string,string] {
    hidden static [Environment] $_Default;

    Environment() {}

    static [Environment] GetDefault() {
        if ([Environment]::_Default -eq $null) {
            [Environment]::_Default = [Environment]::GetCurrent();
        }
        return [Environment]::_Default;
    }

    static [Environment] GetCurrent() {
        $local:Env = [Environment]::new();
        foreach($local:Item in Get-ChildItem "ENV:\") {
            $local:Env[$local:Item.Name] = $local:Item.Value;
        }
        return $local:Env;
    }

    hidden [string] get_Item([string] $Key) {
        $Value = $null;
        [void]($this.TryGetValue($Key, [ref]$Value));
        return $Value;
    }

    [void] Apply() {
        [void]([Environment]::GetDefault());
        $local:Current = [Environment]::GetCurrent();
        foreach ($local:Item in $local:Current.GetEnumerator()) {
            if (-not $this.ContainsKey($local:Item.Key)) {
                script:SetEnvironmentVariable $local:Item.Key $null;
            }
        }
        foreach ($local:Item in $this.GetEnumerator()) {
            script:SetEnvironmentVariable $local:Item.Key $local:Item.Value;
        }
    }

    [Environment] Clone() {
        [Environment] $local:Env = [Environment]::new();
        foreach ($local:Entry in $this.GetEnumerator()) {
            $local:Env[$local:Entry.Key] = $local:Entry.Value;
        }
        return $local:Env;
    }
}

# Stores a diff between two paths
class PathDiff {
    hidden [string[]] $Added;
    hidden [string[]] $Removed;
    hidden [Set] $RemovedSet;

    hidden PathDiff([string[]] $Added, [string[]] $Removed) {
        $this.Added = @() + $Added;
        $this.Removed = @() + $Removed;
        $this.RemovedSet = [Set]::new($Removed);
    }

    static [PathDiff] FromObject([psobject] $Object) {
        if ($Object -eq $null) { return $null; }
        if ($Object -is [PathDiff]) { return $Object; }
        return [PathDiff]::new($Object.Added, $Object.Removed);
    }

    static [psobject] ToObject([PathDiff] $Object) {
        if ($Object -eq $null) { return $null; }
        return @{
            Added = @() + $Object.Added;
            Removed = @() + $Object.Removed;
        };
    }

    static [PathDiff] DiffBetween([string[]] $OldPaths, [string[]] $NewPaths) {
        [Set] $local:OldSet = [Set]::new($OldPaths);
        [Set] $local:NewSet = [Set]::new($NewPaths);
        [string[]] $local:Added = @();
        [string[]] $local:Removed = @();
        foreach ($local:Path in $NewSet.GetEnumerator()) {
            if (-not $OldSet.Contains($local:Path)) {
                $local:Added += $local:Path;
            }
        }
        foreach ($local:Path in $OldSet.GetEnumerator()) {
            if (-not $NewSet.Contains($local:Path)) {
                $local:Removed += $local:Path;
            }
        }
        return [PathDiff]::new($local:Added, $local:Removed);
    }

    [string] Apply([string] $Path) {
        return $this.ApplyToPaths($Path -split ";") -join ";";
    }

    [string[]] Apply([string[]] $Paths) {
        return $this.ApplyToPaths($Paths);
    }

    hidden [string[]] ApplyToPaths([string[]] $Paths) {
        $local:Result = @();
        foreach ($local:Path in $Paths) {
            if ($local:Path -and $local:Path.Trim() -and -not $this.RemovedSet.Contains($local:Path)) {
                $local:Result += $local:Path;
            }
        }
        foreach ($local:Path in $this.Added) {
            if ($local:Path -and $local:Path.Trim()) {
                $local:Result += $local:Path;
            }
        }
        return $local:Result;
    }
}

# Stores a diff between two environments
class EnvironmentDiff : System.Collections.Generic.Dictionary[string,psobject] {
    EnvironmentDiff() { }

    static [EnvironmentDiff] FromObject([psobject] $Object) {
        if ($Object -eq $null) { return $null; }
        if ($Object -is [EnvironmentDiff]) { return $Object; }
        $Object = script:ConvertToHashTable $Object;
        [EnvironmentDiff] $local:Changes = [EnvironmentDiff]::new();
        foreach ($local:Entry in $Object.GetEnumerator()) {
            $local:Key = $local:Entry.Key;
            $local:Value = $local:Entry.Value;
            if ($local:Key -ieq "Path") {
                $local:Value = [PathDiff]::FromObject($local:Value);
            }
            $local:Changes[$local:Key] = $local:Value;
        }
        return $local:Changes;
    }

    static [psobject] ToObject([EnvironmentDiff] $Object) {
        if ($Object -eq $null) { return $null; }
        $local:Changes = @{};
        foreach ($local:Entry in $Object.GetEnumerator()) {
            $local:Key = $local:Entry.Key;
            $local:Value = $local:Entry.Value;
            if ($local:Key -ieq "Path") {
                $local:Value = [PathDiff]::ToObject($local:Value);
            }
            $local:Changes[$local:Key] = $local:Value;
        }
        return $local:Changes;
    }

    static [EnvironmentDiff] DiffBetween([Environment] $OldEnv, [Environment] $NewEnv) {
        [EnvironmentDiff] $local:Changes = [EnvironmentDiff]::new();
        foreach ($local:Entry in $OldEnv.GetEnumerator()) {
            if (-not $NewEnv.ContainsKey($local:Entry.Key)) {
                $local:Changes[$local:Entry.Key] = $null;
            }
        }
        foreach ($local:Entry in $NewEnv.GetEnumerator()) {
            $local:Key = $local:Entry.Key;
            $local:Value = $local:Entry.Value;
            $local:OldValue = $OldEnv[$local:Key];
            if ($local:Value -ne $local:OldValue) {
                if ($local:Key -ieq "Path") {
                    $local:Value = [PathDiff]::DiffBetween($local:OldValue, $local:Value);
                }
                $local:Changes[$local:Key] = $local:Value;
            }
        }
        return $local:Changes;
    }

    hidden [psobject] get_Item([string] $Key) {
        [psobject] $Value = $null;
        [void]($this.TryGetValue($Key, [ref]$Value));
        return $Value;
    }

    hidden [void] set_Item([string] $Key, [psobject] $Value) {
        if (-not $this.ValidateKeyValue($Key, $Value)) { return; }
        ([System.Collections.Generic.Dictionary[string, psobject]]$this)[$Key] = $Value;
    }

    hidden [void] Add([string] $Key, [psobject] $Value) {
        if (-not $this.ValidateKeyValue($Key, $Value)) { return; }
        [void](([System.Collections.Generic.Dictionary[string, psobject]]$this).Add($Key, $Value));
    }

    [Environment] Apply([Environment]$Env) {
        [Environment] $local:NewEnv = $Env.Clone();
        foreach ($local:Entry in $this.GetEnumerator()) {
            $local:Key = $local:Entry.Key;
            $local:Value = $local:Entry.Value;
            if ($local:Value -is [PathDiff]) {
                $local:Value = $local:Value.Apply($Env[$local:Key]);
            }
            if ($local:Value) {
                $local:NewEnv[$local:Key] = $local:Value;
            }
            else {
                $local:NewEnv.Remove($local:Key);
            }
        }
        return $local:NewEnv;
    }

    hidden [bool] ValidateKeyValue([string] $Key, [psobject] $Value) {
        if (($Key -ieq "Path") -and -not ($Value -eq $null -or $Value -is [PathDiff])) {
            throw [System.ArgumentException]::new("Invalid argument: Value");
            return $false;
        }
        if (($Key -ine "Path") -and -not ($Value -eq $null -or $Value -is [string])) {
            throw [System.ArgumentException]::new("Invalid argument: Value");
            return $false;
        }
        return $true;
    }
}

# Represents an instance of Visual Studio
class VisualStudioInstance {
    [string] $Name;
    [string] $Channel;
    [string] $Version;
    [string] $Path;
    hidden [EnvironmentDiff] $Env;

    VisualStudioInstance([string] $Name, [string] $Channel, [string] $Version, [string] $Path, [EnvironmentDiff] $Env) {
        $this.Name = $Name;
        $this.Channel = $Channel;
        $this.Version = $Version;
        $this.Path = $Path;
        $this.Env = $Env;
    }

    static [VisualStudioInstance] FromObject([psobject] $Object) {
        if ($Object -eq $null) { return $null; }
        if ($Object -is [VisualStudioInstance]) { return $Object; }
        return [VisualStudioInstance]::new(
            $Object.Name,
            $Object.Channel,
            $Object.Version,
            $Object.Path,
            [EnvironmentDiff]::FromObject($Object.Env)
        );
    }

    static [psobject] ToObject([VisualStudioInstance] $Object) {
        if ($Object -eq $null) { return $null; }
        return @{
            Name = $Object.Name;
            Channel = $Object.Channel;
            Version = $Object.Version;
            Path = $Object.Path;
            Env = [EnvironmentDiff]::ToObject($Object.Env);
        };
    }

    [EnvironmentDiff] GetEnvironment() {
        if ($this.Env -eq $null) {
            $local:CurrentEnv = [Environment]::GetCurrent();
            $local:DefaultEnvironment = [Environment]::GetDefault();
            $local:DefaultEnvironment.Apply();
            $local:Env = [Environment]::GetCurrent();
            $local:CommandPath = Join-Path $this.Path $script:VSDEVCMD_PATH;
            $local:Command = '"' + ($local:CommandPath) + '"&set';
            cmd /c $local:Command | ForEach-Object {
                if ($_ -match "^(.*?)=(.*)$") {
                    $local:Key = $Matches[1];
                    $local:Value = $Matches[2];
                    $local:Env[$local:Key] = $local:Value;
                }
            }
            $this.Env = [EnvironmentDiff]::DiffBetween($local:DefaultEnvironment, $local:Env);
            $local:CurrentEnv.Apply();
            $script:HasChanges = $true;
        }
        return $this.Env;
    }

    hidden [void] Apply() {
        $this.GetEnvironment().Apply([Environment]::GetDefault()).Apply();
    }

    [void] Save() {
        $script:HasChanges = $true;
        script:SaveChanges;
    }
}

# Converts a JSON object (from ConvertFrom-Json) into a Hashtable
function script:ConvertToHashTable([psobject] $Object) {
    if ($Object -eq $null) { return $null; }
    if ($Object -is [hashtable]) { return $Object };
    $local:Table = @{};
    foreach ($local:Key in $Object | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name) {
        $local:Value = $Object | Select-Object -ExpandProperty $local:Key;
        $local:Table[$local:Key] = $local:Value;
    }
    return $local:Table;
}

# Sets or removes an environment variable
function script:SetEnvironmentVariable([string] $Key, [string] $Value) {
    if ($Value -ne $null) {
        [void](Set-Item -Force "ENV:\$Key" -Value $Value);
    }
    else {
        [void](Remove-Item -Force "ENV:\$Key");
    }
}

# Populates $script:VisualStudioVersions from cache if it is empty
function script:PopulateVisualStudioVersionsFromCache() {
    if ($script:VisualStudioVersions -eq $null) {
        if (Test-Path $script:CACHE_PATH) {
            $script:VisualStudioVersions = (Get-Content $script:CACHE_PATH | ConvertFrom-Json) `
                | ForEach-Object {
                    [VisualStudioInstance]::FromObject($_);
                };
        }
    }
}

# Populates $script:VisualStudioVersions from disk if it is empty
function script:PopulateVisualStudioVersions() {
    if ($script:VisualStudioVersions -eq $null) {
        # Add Legacy instances
        $script:VisualStudioVersions = Get-ChildItem ${env:ProgramFiles(x86)} `
            | Where-Object -Property Name -Match "Microsoft Visual Studio (\d+.0)" `
            | ForEach-Object {
                [VisualStudioInstance]::new(
                    $Matches[0],
                    "Release",
                    $Matches[1],
                    $_.FullName,
                    $null
                );
            };

        # Add Dev15+ instances
        if (Test-Path $script:VS_INSTANCES_DIR) {
            $script:VisualStudioVersions += Get-ChildItem $script:VS_INSTANCES_DIR `
                | ForEach-Object {
                    $local:StatePath = Join-Path $_.FullName "state.json";
                    $local:State = Get-Content $local:StatePath | ConvertFrom-Json;
                    [VisualStudioInstance]::new(
                        $local:State.installationName,
                        $local:State.channelId,
                        $local:State.installationVersion,
                        $local:State.installationPath,
                        $null
                    );
                };
        }

        # Sort by version descending and remove versions that don't exist
        $script:VisualStudioVersions = $script:VisualStudioVersions `
            | Sort-Object -Property Version -Descending `
            | Where-Object { Test-Path (Join-Path $_.Path $script:VSDEVCMD_PATH) };

        if ($script:VisualStudioVersions) {
            $script:HasChanges = $true;
        }
    }
}

# Saves any changes to the $script:VisualStudioVersions cache to disk
function script:SaveChanges() {
    if ($script:HasChanges -and $script:VisualStudioVersions) {
        $local:Content = $script:VisualStudioVersions `
            | ForEach-Object {
                [VisualStudioInstance]::ToObject($_);
            } `
            | ConvertTo-Json;
        if ($script:VisualStudioVersions.Length -eq 1) {
            $local:Content = "[" + $local:Content + "]";
        }
        $local:CacheDir = Split-Path $script:CACHE_PATH -Parent;
        if (-not (Test-Path $local:CacheDir)) {
            [void](mkdir $local:CacheDir -ErrorAction:SilentlyContinue);
        }

        $local:Content | Out-File $script:CACHE_PATH;
        $script:HasChanges = $false;
    }
}

# Indicates whether the specified profile path exists
function script:HasProfile([string] $ProfilePath) {
    if (-not $ProfilePath) { return $false; }
    if (-not (Test-Path -LiteralPath $ProfilePath)) { return $false; }
    return $true;
}

# Indicates whether "posh-vsdev" is referenced in the specified profile
function script:IsInProfile([string] $ProfilePath) {
    if (-not (script:HasProfile $ProfilePath)) { return $false; }
    $local:Content = Get-Content $ProfilePath -ErrorAction:SilentlyContinue;
    if ($local:Content -match "posh-vsdev") { return $true; }
    return $false;
}

# Indicates whether the Use-VisualStudioEnvironment cmdlet is referenced int he specified profile
function script:IsUsingEnvironment([string] $ProfilePath) {
    if (-not (script:HasProfile $ProfilePath)) { return $false; }
    $local:Content = Get-Content $ProfilePath -ErrorAction:SilentlyContinue;
    if ($local:Content -match "Use-VisualStudioEnvironment") { return $true; }
    return $false;
}

# Indicates whether the specified profile is signed
function script:IsProfileSigned([string] $ProfilePath) {
    if (-not (script:HasProfile $ProfilePath)) { return $false; }
    $local:Sig = Get-AuthenticodeSignature $ProfilePath;
    if (-not $local:Sig) { return $false; }
    if (-not $local:Sig.SignerCertificate) { return $false; }
    return $true;
}

# Indicates whether this module is installed within a PowerShell common module path
function script:IsInModulePaths() {
    foreach ($local:Path in $env:PSModulePath -split ";") {
        if (-not $local:Path.EndsWith("\")) { $local:Path += "\"; }
        if ($PSScriptRoot.StartsWith($local:Path, [System.StringComparison]::InvariantCultureIgnoreCase)) {
            return $true;
        }
    }
    return $false;
}

<#
.SYNOPSIS
    Get installed Visual Studio instances.
.DESCRIPTION
    The Get-VisualStudioVersion cmdlet gets information about the installed Visual Studio instances on this machine.
.PARAMETER Name
    Specifies a name that can be used to filter the results.
.PARAMETER Channel
    Specifies a release channel that can be used to filter the results.
.PARAMETER Version
    Specifies a version number that can be used to filter the results.
.INPUTS
    None. You cannot pipe objects to Get-VisualStudioVersion.
.OUTPUTS
    VisualStudioInstance. Get-VisualStudioVersion returns a VisualStudioInstance object for each matching instance.
.EXAMPLE
    PS> Get-VisualStudioVersion
    Name Channel Version Path
    ---- ------- ------- ----
    VisualStudio/15.0.0+26228.9.d15rtwsvc VisualStudio.15.int.d15rel 15.0.26228.9 C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise
    Microsoft Visual Studio 14.0 Release 14.0 C:\Program Files (x86)\Microsoft Visual Studio 14.0
.EXAMPLE
    PS> Get-VisualStudioVersion -Channel Release
    Name Channel Version Path
    ---- ------- ------- ----
    Microsoft Visual Studio 14.0 Release 14.0 C:\Program Files (x86)\Microsoft Visual Studio 14.0
#>

function Get-VisualStudioVersion([string] $Name, [string] $Channel, [string] $Version) {
    script:PopulateVisualStudioVersionsFromCache;
    script:PopulateVisualStudioVersions;
    $local:Versions = $script:VisualStudioVersions;
    if ($Name) {
        $local:Versions = $local:Versions | Where-Object -Property Name -Like $Name;
    }
    if ($Channel) {
        $local:Versions = $local:Versions | Where-Object -Property Channel -Like $Channel;
    }
    if ($Version) {
        $local:Versions = $local:Versions | Where-Object -Property Version -Like $Version;
    }
    $local:Versions;
    script:SaveChanges;
}

<#
.SYNOPSIS
    Uses the developer environment variables for an instance of Visual Studio.
.DESCRIPTION
    The Use-VisualStudioEnvironment cmdlet overwrites the current environment variables with ones from the
    Developer Command Prompt for a specific instance of Visual Studio.
    If a developer environment is already in use, the environment is first reset to the state at the time
    the "posh-vsdev" module was loaded.
.PARAMETER Name
    Specifies a name that can be used to filter the results.
.PARAMETER Channel
    Specifies a release channel that can be used to filter the results.
.PARAMETER Version
    Specifies a version number that can be used to filter the results.
.PARAMETER InputObject
    A VisualStudioInstance whose environment should be used.
.INPUTS
    VisualStudioInstance.
        You can pipe a VisualStudioInstance to Use-VisualStudioEnvironment.
.OUTPUTS
    None.
.EXAMPLE
    PS> Use-VisualStudioVersion
    Using Development Environment from 'Microsoft Visual Studio 14.0'.
#>

function Use-VisualStudioEnvironment {
    [CmdletBinding()]
    param (
        [Parameter(ParameterSetName = "Match")]
        [string] $Name,
        [Parameter(ParameterSetName = "Match")]
        [string] $Channel,
        [Parameter(ParameterSetName = "Match")]
        [version] $Version,
        [Parameter(ParameterSetName = "Pipeline", Position = 0, ValueFromPipeline = $true, Mandatory = $true)]
        [psobject] $InputObject
    );

    [void]([Environment]::GetDefault());
    [VisualStudioInstance] $local:Instance = $null;
    if ($InputObject) {
        $local:Instance = [VisualStudioInstance]::FromObject($InputObject);
    } else {
        $local:Instance = Get-VisualStudioVersion -Name:$Name -Channel:$Channel -Version:$Version | Select-Object -First:1;
    }

    if ($local:Instance) {
        $local:Instance.Apply();
        script:SaveChanges;
        Write-Host "Using Development Environment from '$($local:Instance.Name)'." -ForegroundColor:DarkGray;
        $script:VisualStudioVersion = $local:Instance;
    }
    else {
        [string] $local:Message = "Could not find Visual Studio";
        [string[]] $local:MessageParts = @();
        if ($Name) { $local:MessageParts += "Name='$Name'"; }
        if ($Channel) { $local:MessageParts += "Channel='$Channel'"; }
        if ($Version) { $local:MessageParts += "Version='$Version'"; }
        if ($local:MessageParts.Length > 0) {
            $local:Message += "for " + $local:MessageParts[0];
            if ($local:MessageParts.Length -eq 2) {
            }
            elseif ($local:MessageParts.Length -gt 2) {
                for ($local:I = 1; $local:I -lt $local:MessageParts.Length - 1; $local:I++) {
                    $local:Message += ", " + $local:MessageParts[$local:I];
                }
                if ($local:MessageParts.Length > 2) {
                    $local:Message += ", and " + $local:MessageParts[$local:MessageParts.Length - 1];
                }
            }
        }
        $local:Message += ".";
        Write-Warning $local:Message;
    }
}

<#
.SYNOPSIS
    Restores the original enironment.
.DESCRIPTION
    The Reset-VisualStudioEnvironment cmdlet restores all environment variables to their values
    at the point the "posh-vsdev" module was first imported.
.PARAMETER Force
    Indicates that all environment variables should be restored even if no development environment
    was used.
.INPUTS
    None. You cannot pipe objects to Reset-VisualStudioEnvironment.
.OUTPUTS
    None.
#>

function Reset-VisualStudioEnvironment([switch] $Force) {
    if ($script:VisualStudioVersion -or $Force) {
        $script:VisualStudioVersion = $null;
        [Environment]::GetDefault().Apply();
        Write-Host "Restored default environment" -ForegroundColor DarkGray;
    }
}

<#
.SYNOPSIS
    Resets the cache of installed Visual Studio instances.
.DESCRIPTION
    Resets the cache of installed Visual Studio instances and their respective environment
    settings.
.INPUTS
    None. You cannot pipe objects to Reset-VisualStudioVersionCache.
.OUTPUTS
    None.
#>

function Reset-VisualStudioVersionCache() {
    $script:VisualStudioVersions = $null;
    if (Test-Path $script:CACHE_PATH) {
        [void](Remove-Item $script:CACHE_PATH -Force);
    }
}

<#
.SYNOPSIS
    Adds "posh-vsdev" to your profile.
.DESCRIPTION
    Adds an import to "posh-vsdev" to your PowerShell profile.
.PARAMETER AllHosts
    Specifies that "posh-vsdev" should be installed to your PowerShell profile for all PowerShell hosts.
    If not provided, only the current profile is used.
.PARAMETER UseEnvironment
    Specifies that an invocation of the Use-VisualStudioEnvironment cmdlet should be added to your
    PowerShell profile.
.PARAMETER Force
    Indicates that "posh-vsdev" should be added to your profile, even if it may already be present.
.INPUTS
    None. You cannot pipe objects to Reset-VisualStudioVersionCache.
.OUTPUTS
    None.
#>

function Add-VisualStudioEnvironmentToProfile([switch] $AllHosts, [switch] $UseEnvironment, [switch] $Force) {
    [string] $local:ProfilePath = if ($AllHosts) { $profile.CurrentUserAllHosts; } else { $profile.CurrentUserCurrentHost; }
    [bool] $local:IsInProfile = script:IsInProfile $local:ProfilePath;
    [bool] $local:IsUsingEnvironment = script:IsUsingEnvironment $local:ProfilePath;
    if (-not $Force -and $local:IsInProfile -and -not $UseEnvironment) {
        Write-Warning "'posh-vsdev' is already installed.";
        return;
    }
    if (-not $Force -and $local:IsUsingEnvironment -and $UseEnvironment) {
        Write-Warning "'posh-vsdev' is already using a VisualStudio environment.";
        return;
    }
    if (script:IsProfileSigned $local:ProfilePath) {
        Write-Warning "Cannot modify signed profile.";
        return;
    }
    [string] $local:Content = $null;
    if ($Force -or -not $local:IsInProfile) {
        if (-not (script:HasProfile $local:ProfilePath)) {
            $local:ProfileDir = Split-Path $local:ProfilePath -Parent;
            if (-not (Test-Path -LiteralPath:$local:ProfileDir)) {
                [void](mkdir $local:ProfileDir -ErrorAction:SilentlyContinue);
            }
        }
        if (script:IsInModulePaths) {
            $local:Content += "`nImport-Module posh-vsdev;";
        }
        else {
            $local:Content += "`nImport-Module `"$PSScriptRoot\posh-vsdev.psd1`";";
        }
    }
    if ($Force -or (-not $local:IsUsingEnvironment -and $UseEnvironment)) {
        $local:Content += "`nUse-VisualStudioEnvironment;";
    }
    if ($local:Content) {
        Add-Content -LiteralPath:$local:ProfilePath -Value $local:Content -Encoding UTF8;
    }
}

# constants
[string] $script:VSDEVCMD_PATH = "Common7\Tools\VsDevCmd.bat";
[string] $script:VS_INSTANCES_DIR = "$env:ProgramData\Microsoft\VisualStudio\Packages\_Instances";
[string] $script:CONFIG_DIR = "$env:USERPROFILE\.posh-vsdev";
[string] $script:CACHE_PATH = "$script:CONFIG_DIR\instances.json";

# state
[bool] $script:HasChanges = $false;                             # Indicates whether the in-memory cache has changes
[VisualStudioInstance[]] $script:VisualStudioVersions = $null;  # In-memory cache of instances
[VisualStudioInstance] $script:VisualStudioVersion = $null;     # Current VS instance

# Save the default environment.
[void]([Environment]::GetDefault());

# Reset the environment when the module is removed
$ExecutionContext.SessionState.Module.OnRemove = {
    if ($script:VisualStudioVersion) {
        Reset-VisualStudioEnvironment;
    }
};

# Export members
Export-ModuleMember `
    -Function:(
        'Get-VisualStudioVersion',
        'Use-VisualStudioEnvironment',
        'Reset-VisualStudioEnvironment',
        'Reset-VisualStudioVersionCache',
        'Add-VisualStudioEnvironmentToProfile'
    ) `
    -Variable:(
        'VisualStudioVersion'
    );