Functions/Workspace/Update-Workspace.ps1

<#
    .SYNOPSIS
        Update the workspace configuration for Visual Studio Code which is used
        by the extension Project Manager.

    .DESCRIPTION
        By default the paths $HOME\Profile, $HOME\Workspace and D:\Workspace are
        used to prepare the project list. It's possible to add multiple
        workspace paths to the configuration.

        The module dynamically detects if a subfolder of the workspace is a git
        repository or not. If not, it will recurse for one more level and add
        the following folders as projects.

        All .code-workspace files in the root of the path or in the .vscode
        subfolder are used for grouped Visual Studio Code workspaces for the
        vscode-open-project extension.

    .EXAMPLE
        PS C:\> Update-Workspace
        Update the workspace using the default paths.

    .LINK
        https://github.com/claudiospizzi/ProfileFever
        https://marketplace.visualstudio.com/items?itemName=alefragnani.project-manager
#>

function Update-Workspace
{
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
    param
    (
        # Path to the workspaces. If not specified, the $Env:WORKSPACE_PATH is
        # used if set. Otherwise, the default paths in $HOME\Profile,
        # $HOME\Workspace and D:\Workspace are used.
        [Parameter(Mandatory = $false)]
        [System.String[]]
        $Path,

        # Path to the JSON config file of the Project Manager extension.
        [Parameter(Mandatory = $false)]
        [System.String]
        $ProjectManagerPath = "$Env:AppData\Code\User\globalStorage\alefragnani.project-manager\projects.json",

        # Specify the mode for orphaned projects. By default, the orphaned
        # projects are disabled.
        # - Disabled: The orphaned projects are disabled in the Project Manager configuration.
        # - Remove: The orphaned projects are removed from the Project Manager configuration.
        # - Clean: Start fresh with a new configuration, removing all existing projects and adding only the detected ones.
        [Parameter(Mandatory = $false)]
        [ValidateSet('Disable', 'Remove', 'Clean')]
        [System.String]
        $OrphanedProjectMode = 'Disable'
    )

    try
    {
        if (-not $PSBoundParameters.ContainsKey('Path'))
        {
            if ($Env:WORKSPACE_PATH -and -not [System.String]::IsNullOrWhiteSpace($Env:WORKSPACE_PATH))
            {
                $Path = @($Env:WORKSPACE_PATH -split ';') | Where-Object { -not [System.String]::IsNullOrWhiteSpace($_) }

                Write-Verbose "Using workspace paths from the WORKSPACE_PATH environment variable: $($Path -join ', ')"
            }
            else
            {
                $Path = @()
                if (Test-Path -Path "$HOME\Profile")
                {
                    $Path += "$HOME\Profile"
                }
                if (Test-Path -Path "$HOME\Workspace")
                {
                    $Path += "$HOME\Workspace"
                }
                if (Test-Path -Path "D:\Workspace")
                {
                    $Path += "D:\Workspace"
                }

                if ($Path.Count -eq 0)
                {
                    throw 'No workspace path specified and no default path found. Please specify a workspace path or set the WORKSPACE_PATH environment variable.'
                }

                Write-Verbose "Using default workspace paths: $($Path -join ', ')."
            }
        }
        else
        {
            Write-Verbose "Using workspace paths from the parameter: $($Path -join ', ')"
        }

        if ((Test-Path -Path $ProjectManagerPath) -and $OrphanedProjectMode -ne 'Clean')
        {
            $config = @(Get-Content -Path $ProjectManagerPath -Encoding 'UTF8' | ConvertFrom-Json)
        }
        else
        {
            $config = @()
        }

        foreach ($currentPath in $Path)
        {
            Write-Verbose "Processing workspace path: $currentPath"

            if (-not (Test-Path -Path $currentPath))
            {
                Write-Warning "The path '$currentPath' does not exist. Skipping."
                continue
            }

            $currentPath = Get-Item -Path $currentPath

            $detectedPaths = @()

            # Add VS Code workspaces as projects
            $detectedPaths +=
                Get-ChildItem -Path $currentPath -Filter '*.code-workspace' -File |
                    Select-Object @{ N = 'BaseName'; E = { "Workspace $($_.BaseName)" } }, 'FullName', @{ N = 'Tags'; E = { @($currentPath.BaseName) } }
            if (Test-Path -Path "$($currentPath.FullName)\.vscode")
            {
                $detectedPaths +=
                    Get-ChildItem -Path "$($currentPath.FullName)\.vscode" -Filter '*.code-workspace' -File |
                        Select-Object @{ N = 'BaseName'; E = { "Workspace $($_.BaseName)" } }, 'FullName', @{ N = 'Tags'; E = { @($currentPath.BaseName) } }
            }

            # Add root git folder as projects. If the root folder is not a git
            # repository, add all child git folders as projects.
            if (Test-Path -Path "$currentPath\.git")
            {
                $detectedPaths += [PSCustomObject] @{
                    BaseName = $currentPath.BaseName
                    FullName = $currentPath.FullName
                    Tags     = @($currentPath.BaseName)
                }
            }
            else
            {
                # Direct child git folders as projects. Child folders without a
                # .git folder are not added as projects, but their child folders
                # are checked for git repositories.
                $detectedPaths +=
                    Get-ChildItem -Path $currentPath.FullName -Directory |
                        Where-Object { Test-Path -Path "$($_.FullName)\.git" } |
                            Select-Object 'BaseName', 'FullName', @{ N = 'Tags'; E = { @($currentPath.BaseName) } }

                # Add child git folders as projects
                $detectedPaths +=
                    Get-ChildItem -Path $currentPath.FullName -Directory |
                        Where-Object { -not (Test-Path -Path "$($_.FullName)\.git") } |
                            ForEach-Object {
                                $childPath = $_
                                Get-ChildItem -Path $childPath.FullName -Directory |
                                    Select-Object 'BaseName', 'FullName', @{ N = 'Tags'; E = { @($currentPath.BaseName, $childPath.BaseName) } }
                            }
            }

            foreach ($detectedPath in $detectedPaths)
            {
                Write-Verbose "Processing detected path: $($detectedPath.FullName)"

                # Add a new project if it does not exist
                if ($config.Count -eq 0 -or $config.rootPath -notcontains $detectedPath.FullName)
                {
                    $config += [PSCustomObject] @{
                        name     = $detectedPath.BaseName
                        rootPath = $detectedPath.FullName
                        paths    = @()
                        tags     = @($detectedPath.Tags -join ' / ')
                        enabled  = $true
                    }
                }
                else
                {
                    # Search for the existing entry and patch the details.
                    for ($i = 0; $i -lt $config.Count; $i++)
                    {
                        if ($config[$i].rootPath -eq $detectedPath.FullName)
                        {
                            $config[$i] = [PSCustomObject] @{
                                name     = $detectedPath.BaseName
                                rootPath = $detectedPath.FullName
                                paths    = @()
                                tags     = @($detectedPath.Tags -join ' / ')
                                enabled  = $true
                            }
                            break
                        }
                    }
                }
            }
        }

        # Remove or disable all projects not existing on the file system.
        switch ($OrphanedProjectMode)
        {
            'Disable'
            {
                for ($i = 0; $i -lt $config.Count; $i++)
                {
                    if (-not (Test-Path -Path $config[$i].rootPath))
                    {
                        $config[$i].enabled = $false
                    }
                }
            }
            'Remove'
            {
                $config = @($config | Where-Object { Test-Path -Path $_.rootPath })
            }
        }

        # Use the .NET class to write the JSON file to prevent writing the UTF-8
        # BOM header, as the Project Manager is not able to read the profile
        # file with an UTF-8 BOM header.
        if ($PSCmdlet.ShouldProcess($ProjectManagerPath, 'Update the Project Manager workspace configuration.'))
        {
            [System.IO.File]::WriteAllLines($ProjectManagerPath, ($config | ConvertTo-Json))
        }
    }
    catch
    {
        $PSCmdlet.ThrowTerminatingError($_)
    }
}