lib/Extensions.ps1

function Get-TMExtension {
    <#
    .SYNOPSIS
    Gets/Downloads a Reference Design from the TDS BitBucket tm-reference-designs repo

    .DESCRIPTION
    This function

    .PARAMETER Name
    The name of the Reference Design to download

    .PARAMETER Credential
    The credentials used to log into BitBucket

    .PARAMETER SkipDependencies
    Switch indicating that the Reference Designs on which the named Reference Design
    depends on should not be downloaded

    .PARAMETER ManifestUri
    The uri of the raw rd-manifest.json. Defaults to the file in the __Releases directory

    .PARAMETER Dev
    Switch indicating that all reference designs should be searched/retrieved rather
    than just the published ones

    .PARAMETER ProviderName
    One or more specific Providers to get. All other Providers will be removed from the
    downloaded Reference Design

    .EXAMPLE
    $Credential = Get-StoredCredential -Name 'ME'
    Get-TMExtension -Name 'vmware-vcenter' -Credential $Credential -Dev

    .OUTPUTS
    None
    #>


    [alias('Get-TMReferenceDesign')]
    [CmdletBinding(DefaultParameterSetName = 'Console')]
    param (
        [Parameter(Mandatory = $false, Position = 0, ParameterSetName = 'Console')]
        [String[]]$Name,

        [Parameter(Mandatory = $false, Position = 1)]
        [PSCredential]$Credential,

        [Parameter(Mandatory = $false)]
        [Switch]$SkipDependencies,

        [Parameter(Mandatory = $false)]
        [String]$ManifestUri,

        [Parameter(Mandatory = $false)]
        [Switch]$Dev,

        [Parameter(Mandatory = $false)]
        [String[]]$ProviderName

        # [Parameter(Mandatory = $false, ParameterSetName = "UI")]
        # [Switch]$ShowMenu
        # TODO: Consider if we could add a $Path/$Location type
        # parameter to allow the user to specify a filesystem
    )

    # Some of the TMD functions will be needed in this function
    if (!( Get-Module 'TMD.Common')) {
        Import-Module 'TMD.Common'
    }

    ## Create a $PackageNames variable with all of the Reference Design names (Lower, no spaces)
    $PackageNames = $Name.ToLower() -replace ' - ', '-' -replace ' ', '-'

    ## Save the Progress Preference now so that it can be changed and set back later
    $ProgressPreferenceBackup = $Global:ProgressPreference
    $Global:ProgressPreference = 'SilentlyContinue'

    $ExtensionFolder = Join-Path $Global:userFilesRoot 'Reference Designs'
    $LocalExtensions = [System.Collections.ArrayList]::new()
    $ExtensionsToDownload = [System.Collections.ArrayList]::new()
    # $ShouldInstall = $false

    ## TODO: I don't like this credential handling. If a user supplies a credential, it should not be written to disk, but only used.
    ## To mitigate this, you could do a few things -StoreCredentialAs (Name), or
    ## -CredentialName {Which would hand off to Get-StoredCredential}

    # Check if the credential is already cached
    $CredentialPath = Join-Path -Path $Global:UserPaths.credentials -ChildPath 'Bitbucket.credential'
    if (!$Credential) {
        $Credential = (Test-Path -Path $CredentialPath) ? (Get-StoredCredential -Name 'Bitbucket') : (Get-Credential)
    }

    # Save the credential for future use
    Add-StoredCredential -Name 'Bitbucket' -Credential $Credential
    Write-Verbose 'Credential was stored as Bitbucket.credential'

    # Get the reference design manifest from bitbucket
    Write-Host 'Getting available Reference Designs from Bitbucket...'
    try {
        if ([String]::IsNullOrWhiteSpace($ManifestUri)) {
            $ManifestUri = "https://git.tdsops.com/projects/TM/repos/tm-reference-designs/raw/__Releases/$(if ($Dev) {'dev-'})rd-manifest.json?at=refs%2Fheads%2Fmain"
        }
        $AvailableExtensions = Invoke-RestMethod -Method Get -Uri $ManifestUri -Authentication Basic -Credential $Credential

    } catch {
        throw "Couldn't get the manifest file. Check credentials and try again"
    }
    Write-Host "`tFound " -NoNewline
    Write-Host "$($AvailableExtensions.Count)" -NoNewline -ForegroundColor DarkBlue
    Write-Host ' available Reference Design(s)'

    # Fix the version so that it can be compared later in the script
    for ($i = 0; $i -lt $AvailableExtensions.Count; $i++) {
        $AvailableExtensions[$i].Version = ConvertTo-SemanticVersion -VersionString $AvailableExtensions[$i].Version
        for ($j = 0; $j -lt $AvailableExtensions[$i].extension.dependencies.extensions.Count; $j++) {
            $AvailableExtensions[$i].extension.dependencies.extensions[$j].version = ConvertTo-SemanticVersion -VersionString $AvailableExtensions[$i].extension.dependencies.extensions[$j].version
        }
    }

    # Either show the UI to get the user's selections or
    switch ($PSCmdlet.ParameterSetName) {
        'Console' {
            Write-Verbose 'Console mode was selected'
            # If no name was supplied, just output the Extension objects
            if (!$PackageNames) {
                $AvailableExtensions | ForEach-Object {
                    $_
                }
                return
            } else {
                $PackageNames | ForEach-Object {
                    $Extension = $AvailableExtensions | Where-Object name -EQ $_ |
                        Sort-Object -Property version | Select-Object -Last 1
                        if ($Extension) {
                            [void]$ExtensionsToDownload.Add($Extension)
                        }
                    }
                }
            }
            <#
        "UI" {
            Write-Verbose "UI mode was selected"
            # $ExePath = "..\exe\RD Installer.exe"
            $ExePath = "C:\DEV\git\tm-reference-designs\_ReferenceDesignManager\ReferenceDesignManager\bin\Debug\net5.0-windows\ReferenceDesignManager.exe"
            $StartInfo = [System.Diagnostics.ProcessStartInfo]::new()
            $StartInfo.RedirectStandardError = $true
            $StartInfo.RedirectStandardOutput = $true
            $StartInfo.FileName = $ExePath
            # $StartInfo.ArgumentList = @(
            # "--repocredential `"$CredentialPath`""
            # "--powershell"
            # )
            $StartInfo.Arguments = "--repocredential `"$CredentialPath`" --powershell"

            $ExtensionInstallerProc = [System.Diagnostics.Process]::new()
            $ExtensionInstallerProc.StartInfo = $StartInfo

            # Start the UI and wait for it to exit so that the stdout can be parsed
            Write-Host "Showing UI..."
            $ExtensionInstallerProc.Start()
            $ExtensionInstallerProc.WaitForExit()
            $StdOut = $ExtensionInstallerProc.StandardOutput.ReadToEnd()

            if ([String]::IsNullOrWhiteSpace($StdOut)) {
                throw "Output was not received from $(Split-Path $ExePath -Leaf)"
            }

            $UserChoices = $StdOut | ConvertFrom-Json
            $UserChoices.ExtensionsToDownload | ForEach-Object {
                [void]$ExtensionsToDownload.Add(
                    @{
                        Name = $_
                        Version = $null
                    }
                )
            }
            $ShouldInstall = $UserChoices.ShouldInstall
        }
        #>

        }

        # Make sure one or more available Reference Designs were found
        if ($ExtensionsToDownload.Count -eq 0) {
            throw "No available Reference Designs were found with the specified name(s): $($PackageNames.ToString())"
        }

        ## Create the Destination Downloaded Folder
        $DownloadedFolder = Join-Path $ExtensionFolder 'Downloaded'
        Test-FolderPath $DownloadedFolder

        # Get all of the Reference Designs that are already downloaded
        Write-Host 'Checking for downloaded Reference Designs...'
        Write-Verbose "Looking in '$DownloadFolder'"
        try {
            Get-ChildItem -Path $DownloadedFolder -Filter 'package.json' -Recurse -ErrorAction SilentlyContinue |
                Get-Content -Raw | ConvertFrom-Json | ForEach-Object {
                    [void]$LocalExtensions.Add($_)
                }
    } catch {
        Write-Verbose $_
    }

    Write-Host "`tFound " -NoNewline
    Write-Host "$($LocalExtensions.Count)" -NoNewline -ForegroundColor DarkBlue
    Write-Host ' local Reference Design(s)'

    # Check if the selected Reference Designs have any dependencies on other Reference Designs
    if (!$SkipDependencies) {
        Write-Host 'Checking for Reference Design dependencies...'
        $Count = $ExtensionsToDownload.Count
        for ($i = 0; $i -lt $Count; $i++) {
            $Design = $ExtensionsToDownload[$i]
            foreach ($Dependency in $Design.extension.dependencies.extensions) {
                $AvailableExtension = $AvailableExtensions | Where-Object Name -EQ $Dependency.name |
                    Sort-Object -Property version | Select-Object -Last 1
                if ($AvailableExtension) {
                    $LocalExtension = $LocalExtensions | Where-Object Name -EQ $Dependency.name |
                        Sort-Object -Property version | Select-Object -Last 1
                    if ($LocalExtension) {
                        if ($LocalExtension.version -lt $Dependency.version) {
                            Write-Host "`tFound Reference Design to update: " -NoNewline
                            Write-Host "$($AvailableExtension.Name) - v$($AvailableExtension.Version.ToString())"
                            [void]$ExtensionsToDownload.Add($AvailableExtension)
                        }
                    } else {
                        Write-Host "`tFound additional Reference Design to download: " -NoNewline
                        Write-Host "$($AvailableExtension.Name) - v$($AvailableExtension.Version.ToString())"
                        [void]$ExtensionsToDownload.Add($AvailableExtension)
                    }
                }
            }
        }
    }

    # Start downloading the named reference designs
    Write-Host 'Downloading Reference Designs...'
    # $EncounteredError = $false
    foreach ($ExtensionSpec in $ExtensionsToDownload) {
        Write-Host "`tProcessing: " -NoNewline
        Write-Host $ExtensionSpec.ProductName -ForegroundColor DarkBlue

        ## Resolve the file name and the destination path
        $ZipFileName = $ExtensionSpec.Name + '-' + $ExtensionSpec.Version + '.zip'
        $Url = 'https://git.tdsops.com/projects/TM/repos/tm-reference-designs/raw/__Releases/{0}?at=refs%2Fheads%2Fmain' -f $ZipFileName
        $DestinationFilePath = Join-Path $ExtensionFolder 'Downloaded' $ZipFileName

        try {
            ## Call to Bitbucket to download the file
            Write-Host "`t`tDownloading $ZipFileName ($([Math]::Round(($ExtensionSpec.Size / 1MB), 2)) MB)..." -NoNewline
            Invoke-WebRequest -Uri $Url -Credential $Credential -Authentication Basic -Method Get -OutFile $DestinationFilePath
            Write-Host 'Done' -ForegroundColor Green
        } catch {
            Write-Error $_
            continue
        }

        try {
            # Unzip the downloaded file
            Write-Host "`t`tExpanding archive..." -NoNewline
            $DestinationDirectory = Join-Path $ExtensionFolder 'Downloaded' $ExtensionSpec.ProductName
            Expand-Archive -Path $DestinationFilePath -DestinationPath $DestinationDirectory -Force
            Write-Host 'Done' -ForegroundColor DarkGreen
        } catch {
            Write-Error $_
            continue
        }

        # Remove the .zip file now that its contents been extracted
        Remove-Item -Path $DestinationFilePath -Force

        if ($PSBoundParameters.ContainsKey('ProviderName')) {
            # Remove the actions and Datascripts that don't belong to the specified providers
            $ScriptPaths = @(
                (Join-Path $DestinationDirectory '_REFERENCE DESIGN' 'Actions')
                (Join-Path $DestinationDirectory '_REFERENCE DESIGN' 'ETL')
            )
            Get-ChildItem -Path $ScriptPaths -Directory -Exclude $ProviderName -ErrorAction SilentlyContinue | Remove-Item -Recurse -Force

            # Remove the providers from providers.json that weren't specified in ProviderName
            $FilteredProviders = [System.Collections.ArrayList]::new()
            $ProviderFile = Join-Path $DestinationDirectory '_REFERENCE DESIGN' 'Providers.json'
            $Providers = , @(Get-Content -Path $ProviderFile | ConvertFrom-Json -Depth 10)
            foreach ($Provider in $Providers) {
                if ($Provider.name -in $ProviderName) {
                    [void]($FilteredProviders.Add($Provider))
                }
            }
            ConvertTo-Json -Depth 10 -InputObject $FilteredProviders | Set-Content -Path $ProviderFile -Force
        }
    }

    ## Set the progress preference back
    $Global:ProgressPreference = $ProgressPreferenceBackup

    # if ($ShouldInstall -and !$EncounteredError) {
    # Install-TMExtension -Name $UserChoices.ExtensionsToDownload
    # }
}


function Get-TMLocalExtension {
    <#
    .SYNOPSIS
    Gets one or more Reference Designs that have been downloaded loacally

    .DESCRIPTION
    This function scans the C:\Users\<user>\TMD_Files\Reference Designs directory
    for the package.json file(s) associated with Reference Designs that have been
    downloaded locally

    .PARAMETER Name
    The name(s) of the downloaded Reference Design(s) to retrieve

    .PARAMETER List
    Switch indicating that only a list of downloaded Reference Design names should be returned

    .EXAMPLE
    Get-TMLocalExtension -Name 'vmware-vcenter', 'vmware-hcx'

    .EXAMPLE
    Get-TMLocalExtension -List

    .OUTPUTS
    An array of strings representing the downloaded Reference Design names if the -List switch is used
    otherwise a PSCustomObject for each name that was passed
    #>

    [alias('Get-TMLocalReferenceDesign')]
    [CmdletBinding(DefaultParameterSetName = 'Named')]
    [OutputType([PSCustomObject[]], ParameterSetName = 'Named')]
    [OutputType([String[]], ParameterSetName = 'List')]
    param (
        [Parameter(
            Mandatory = $false,
            ParameterSetName = 'Named',
            ValueFromPipeline = $true)]
        [String[]]$Name,

        [Parameter(Mandatory = $false, ParameterSetName = 'List')]
        [Switch]$List
    )

    begin {
        if (!(Get-Module 'TMD.Common')) {
            Import-Module 'TMD.Common'
        }
        $ExtensionDirectory = Join-Path $Global:userFilesRoot 'Reference Designs'
        $LocalExtensions = Get-ChildItem -Path $ExtensionDirectory -Filter 'package.json' -Recurse -Force -ErrorAction SilentlyContinue | ForEach-Object {
            $Package = Get-Content -Path $_.FullName -Raw | ConvertFrom-Json -Depth 100
            Add-Member -InputObject $Package -Name 'path' -Value (Split-Path $_.FullName) -MemberType NoteProperty -PassThru -Force
        }
    }

    process {
        if ($List) {
            $LocalExtensions | ForEach-Object {
                $_.Name
            }
        } else {
            if ($Name) {
                $Name | ForEach-Object {
                    $LocalExtensions | Where-Object Name -EQ $_
                }
            } else {
                $LocalExtensions
            }
        }
    }
}


function Import-TMExtension {
    <#
    .SYNOPSIS
    Imports a downloaded Reference Design to the specified project on the specified server

    .DESCRIPTION
    This function will apply the different configuration components of
    a Reference Design into a TransitonManager project.

    .PARAMETER Name
    The name of the locally downloaded Reference Design to apply/install

    .PARAMETER TMSession
    The name of the TM Session to use when creating a Team

    .PARAMETER Project
    The project in which the Reference Design will be applied

    .PARAMETER Include
    An array of components to include when performing the export.
    Options are:
        *
        ServerSettings
        ProjectSettings
        Code
        DependencyTypes
        FieldSpecs
        Bundles
        Providers
        Actions
        DataScripts
        Recipes
        AssetViews
        Events
        AppTypes
        DeviceTypes
        Teams
        TaskCategories
    NOTE: ServerSettings, ProjectSettings and Code are not components, but sets of components that are expanded to the following during runtime:
        ServerSettings: App Types, Dependency Types, Device Types, Teams, and Task Categories
        ProjectSettings: Asset Views, Bundles, Events, FieldSpecs, Providers, and Tags
        Code: Actions, DataScripts, Recipes, and (On Import Only) SetupScripts.

    .PARAMETER Exclude
    An array of components to skip when performing the import.
    Options are:
        ServerSettings
        ProjectSettings
        Code
        DependencyTypes
        FieldSpecs
        Bundles
        Providers
        Actions
        Datascripts
        Recipes
        Tags
        AssetViews
        Events
        AppTypes
        DeviceTypes
        Teams
        TaskCategories
        SetupScripts
    NOTE: ServerSettings, ProjectSettings and Code are not components, but sets of components that are expanded to the following during runtime:
        ServerSettings: App Types, Dependency Types, Device Types, Teams, and Task Categories
        ProjectSettings: Asset Views, Bundles, Events, FieldSpecs, Providers, and Tags
        Code: Actions, DataScripts, Recipes, and (On Import Only) SetupScripts.

    .PARAMETER SkipDependencies
    Switch indicating that the Reference Designs on which the named Reference Design
    depends on should not be imported

    .PARAMETER ProviderName
    One or more specific Providers to import. All other Providers will not be
    imported into the project

    .EXAMPLE
    $Credential = Get-StoredCredential -Name 'ME'
    $ImportSplat = @{
        Name = 'vmware-vcenter'
        Project = 'New Project - VMware vCenter'
        TMSession = 'TMAD60'
    }
    Import-TMExtension @ImportSplat

    .OUTPUTS
    None
    #>


    [alias('Import-TMReferenceDesign')]
    [CmdletBinding(DefaultParameterSetName = 'ByInclusion')]
    param (
        [Parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipeline = $true)]
        [ArgumentCompleter( { Get-TMLocalExtension -List } )]
        [String[]]$Name,

        [Parameter(Mandatory = $false, Position = 1)]
        [String]$TMSession = 'Default',

        [Parameter(Mandatory = $false, Position = 2)]
        [String]$Project,

        [Parameter(Mandatory = $false, Position = 3, ParameterSetName = 'ByExclusion')]
        [ValidateSet(
            'ServerSettings',
            'ProjectSettings',
            'Code',
            'DependencyTypes',
            'FieldSpecs',
            'Bundles',
            'Providers',
            'Actions',
            'DataScripts',
            'Recipes',
            'Tags',
            'AssetData',
            'AssetViews',
            'Events',
            'AppTypes',
            'DeviceTypes',
            'Teams',
            'TaskCategories',
            'SetupScripts'
        )]
        [String[]]$Exclude,

        [Parameter(Mandatory = $false, Position = 3, ParameterSetName = 'ByInclusion')]
        [ValidateSet(
            '*',
            'ServerSettings',
            'ProjectSettings',
            'Code',
            'DependencyTypes',
            'FieldSpecs',
            'Bundles',
            'Providers',
            'Actions',
            'DataScripts',
            'Recipes',
            'Tags',
            'AssetData',
            'AssetViews',
            'Events',
            'AppTypes',
            'DeviceTypes',
            'Teams',
            'TaskCategories',
            'SetupScripts'
        )]
        [String[]]$Include = '*',

        [Parameter(Mandatory = $false)]
        [Switch]$SkipDependencies,

        [Parameter(Mandatory = $false)]
        [String[]]$ProviderName
    )

    begin {

        # As per TM-22256, ServerSettings, ProjectSettings, and Code will be used to contain a set of components to exclude/include.
        $ServerSettings = 'AppTypes',
        'DependencyTypes',
        'DeviceTypes',
        'Teams',
        'TaskCategories'
        $ProjectSettings = 'AssetViews',
        'Bundles',
        'Events',
        'FieldSpecs',
        'Providers',
        'Tags'
        $Code = 'Actions',
        'DataScripts',
        'Recipes',
        # (On Import Only)
        'SetupScripts'
        if ($Include -contains 'ServerSettings') { $Include += $ServerSettings }
        if ($Include -contains 'ProjectSettings') { $Include += $ProjectSettings }
        if ($Include -contains 'Code') { $Include += $Code }
        if ($Exclude -contains 'ServerSettings') { $Exclude += $ServerSettings }
        if ($Exclude -contains 'ProjectSettings') { $Exclude += $ProjectSettings }
        if ($Exclude -contains 'Code') { $Exclude += $Code }

        # Get the session configuration
        Write-Verbose 'Checking for cached TMSession'
        $TMSessionConfig = $global:TMSessions[$TMSession]
        Write-Debug 'TMSessionConfig:'
        Write-Debug ($TMSessionConfig | ConvertTo-Json -Depth 5)
        if (-not $TMSessionConfig) {
            throw "TMSession '$TMSession' not found. Use New-TMSession command before using features."
        }

        if (!(Get-Module 'TMD.Common')) {
            Import-Module 'TMD.Common'
        }

        $FilterByProvider = $PSBoundParameters.ContainsKey('ProviderName')

        ## Tag Color Map
        ## PowerShell Colors:
        ## Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White
        ## TM Tag Colors:
        ## 'Grey', 'Red', 'Orange', 'Yellow', 'Green', 'Blue', 'Cyan', 'Purple', 'Pink', 'White'
        $TagColorToForeground = @{
            'Grey'   = 'DarkGray'
            'Red'    = 'Red'
            'Orange' = 'DarkYellow'
            'Yellow' = 'Yellow'
            'Green'  = 'Green'
            'Blue'   = 'Blue'
            'Cyan'   = 'Cyan'
            'Purple' = 'DarkMagenta'
            'Pink'   = 'Magenta'
            'White'  = 'White'
        }

        # Set up a configuration object to determine which components to install later in the process block
        $Components = @{
            DependencyTypes = $true
            FieldSpecs      = $true
            Bundles         = $true
            Providers       = $true
            Actions         = $true
            Datascripts     = $true
            Recipes         = $true
            Tags            = $true
            AssetViews      = $true
            Events          = $true
            AppTypes        = $true
            DeviceTypes     = $true
            Teams           = $true
            TaskCategories  = $true
            SetupScripts    = $true
        }

        switch ($PSCmdlet.ParameterSetName) {
            'ByInclusion' {
                if ($Include -notcontains '*') {
                    foreach ($Key in $Components.GetEnumerator().Name) {
                        if ($Include -notcontains $Key) {
                            $Components.$Key = $false
                        }
                    }
                }
            }

            'ByExclusion' {
                # Process the Exclude parameter and turn components off
                $ComponentSets = 'ServerSettings', 'ProjectSettings', 'Code'
                $Exclude | ForEach-Object {
                    #Component sets are not actual components, so we don't want to process them.
                    if ($_ -notin $ComponentSets) {
                        $Components.$_ = $false
                    }
                }
            }
        }

        # Get a list of all of the Reference Designs that have been saved locally
        $ExtensionDirectory = Join-Path $Global:userFilesRoot 'Reference Designs'
        $LocalExtensions = Get-TMLocalExtension
        if (!$LocalExtensions) {
            throw "No locally saved reference designs were found in $ExtensionDirectory. Use Get-TMExtension to download before installing."
        }

        # Determine the project to export
        if (-not [String]::IsNullOrWhiteSpace($Project) -and ($TMSessionConfig.userContext.project.name -ne $Project)) {
            Enter-TMProject -ProjectName $Project -TMSession $TMSession
        } else {
            $Project = $TMSessionConfig.userContext.project.name
        }
    }

    process {
        # Get the Extension spec from the passed Names and verify they are already downloaded
        $ExtensionToInstall = [System.Collections.Stack]::new()
        $DependencyCheckQueue = [System.Collections.Queue]@()
        foreach ($ExtensionName in $Name) {
            $ExtensionName = $ExtensionName.ToLower().Replace(' - ', '-').Replace(' ', '-')
            $Extension = $LocalExtensions | Where-Object Name -EQ $ExtensionName
            if (!$Extension) {
                throw "The reference design $ExtensionName has not been downloaded locally. Use Get-TMExtension and try again."
            }

            ## Check for Dependencies to add to the list before the Extension being installed
            if (!$SkipDependencies) {

                ## Add This Extension to the DependencyCheckList
                $DependencyCheckQueue.Enqueue($Extension)
            }

            ## Add the Extension being installed to the list, after dependencies are added
            $ExtensionToInstall.Push($Extension)
        }

        ## Check every Reference Design for Dependencies
        if (-Not $SkipDependencies) {

            Write-Host 'Checking Dependency Requirements'
            while ($DependencyCheckQueue.Count -ne 0) {

                # ## Check Each of the Extensions that are queued for installation for Dependencies
                # foreach ($ExtensionCheck in $ExtensionsToCheckDependenciesFor) {
                $ExtensionCheck = $DependencyCheckQueue.Dequeue()

                ## Remove Placeholder tokens provided by the template file creation
                $DependentExtensions = @()
                $ExtensionCheck.referenceDesign.dependencies.referenceDesigns | Where-Object { $_.name -ne '<PLACEHOLDER>' } | ForEach-Object {
                    if ($_) { $DependentExtensions += $_ }
                }
                $ExtensionCheck.extension.dependencies.extensions | Where-Object { $_.name -ne '<PLACEHOLDER>' } | ForEach-Object {
                    if ($_) { $DependentExtensions += $_ }
                }

                foreach ($DependencyItem in $DependentExtensions) {

                    ## Get the Highest version number that matches the requirement
                    $Dependency = $LocalExtensions
                    | Where-Object { $_.Name -eq $DependencyItem.Name -and $_.Version -ge $DependencyItem.Version }
                    | Sort-Object -Property 'version' -Descending
                    | Select-Object -First 1

                    ## Ensure a dependency was found
                    if (!$Dependency) {
                        throw "The dependency Reference Design $($DependencyItem.Name) (Minimum v$($DependencyItem.Version)) was not found on the local hard drive. Use Get-TMExtension and try again."
                    }

                    if ($ExtensionToInstall -notcontains $Dependency) {
                        Write-Host 'Required Prerequisite Extension also will be installed: ' -NoNewline
                        Write-Host $Dependency.name -ForegroundColor Cyan -NoNewline
                        Write-Host " v$($Dependency.version)" -ForegroundColor DarkGray
                        # $ExtensionToInstall += $Dependency
                        $ExtensionToInstall.Push($Dependency)
                        [void]($DependencyCheckQueue.Enqueue($Dependency))
                    }
                }
            }
        }
        ## Create an array of Setup Scripts to run
        $SetupScriptsToRun = [System.Collections.ArrayList]@()

        ## Iterate over each Reference Design to Install
        while ($ExtensionToInstall.Count) {

            ## Get the next item from the Stack
            $Extension = $ExtensionToInstall.Pop()

            Write-Host 'Importing: ' -NoNewline
            Write-Host $Extension.productName -NoNewline -ForegroundColor Cyan
            Write-Host ', Version: ' -NoNewline
            Write-Host $Extension.Version -ForegroundColor DarkGray

            $ExtensionPath = Join-Path $Extension.Path '_REFERENCE DESIGN'

            # Create the dependency types
            if ($Components.DependencyTypes) {
                Write-Host 'Loading Dependency Types...'
                $DependencyTypes = [system.collections.arraylist]@()
                $DependencyTypesFolderPath = Join-Path $ExtensionPath 'DependencyTypes'
                if (Test-Path $DependencyTypesFolderPath -ErrorAction SilentlyContinue) {
                    $DependencyTypeFiles = Get-ChildItem -Path $DependencyTypesFolderPath -ErrorAction SilentlyContinue
                    $DependencyTypeFiles | ForEach-Object {
                        [void]($DependencyTypes.Add((Get-Content -Path $_.FullName | ConvertFrom-Json)))
                    }

                } else {
                    $DependencyTypesFilePath = Join-Path $ExtensionPath 'DependencyTypes.json'
                    if (Test-Path $DependencyTypesFilePath -ErrorAction SilentlyContinue) {
                        $DependencyTypes = Get-ChildItem -Path $DependencyTypesFilePath -ErrorAction SilentlyContinue | Get-Content | ConvertFrom-Json
                    }
                }

                ## Check if there were any entries
                if ($DependencyTypes.Count -eq 0) {
                    Write-Host "`tNo Dependency Types to add"
                } else {

                    ## Get the Server's existing Dependency Types
                    $ServerDependencyTypes = Get-TMDependencyType -TMSession $TMSession
                    foreach ($DependencyType in $DependencyTypes) {

                        ## If the server doesn't have the dependency type
                        if ($ServerDependencyTypes.label -notcontains $DependencyType.label) {

                            ## Add it to the server
                            Write-Host "`tLoading Dependency Type: " -NoNewline
                            Write-Host "$($DependencyType.label)" -ForegroundColor DarkBlue
                            $DependencyType = [TMDependencyType]::new($DependencyType.label)
                            New-TMDependencyType -TMSession $TMSession -DependencyType $DependencyType
                        }
                    }
                }
            }

            # Create the App types
            if ($Components.AppTypes) {
                Write-Host 'Loading App Types...'
                $AppTypes = [system.collections.arraylist]@()
                $AppTypesFolderPath = Join-Path $ExtensionPath 'AppTypes'
                if (Test-Path $AppTypesFolderPath -ErrorAction SilentlyContinue) {
                    $AppTypeFiles = Get-ChildItem -Path $AppTypesFolderPath -ErrorAction SilentlyContinue
                    $AppTypeFiles | ForEach-Object {
                        [void]($AppTypes.Add((Get-Content -Path $_.FullName | ConvertFrom-Json)))
                    }
                } else {
                    $AppTypesFilePath = Join-Path $ExtensionPath 'AppTypes.json'
                    if (Test-Path $AppTypesFilePath -ErrorAction SilentlyContinue) {
                        $AppTypes = Get-Content -Path $AppTypesFilePath | ConvertFrom-Json
                    }
                }

                ## Check if there were any entries
                if ($AppTypes.Count -eq 0) {
                    Write-Host "`tNo App Types to add"
                } else {

                    ## Get the Server's existing App Types
                    $ServerAppTypes = Get-TMAssetOption -Type 'App Type' -TMSession $TMSession
                    foreach ($AppType in $AppTypes) {

                        ## If the server doesn't have the App type
                        if ($ServerAppTypes.label -notcontains $AppType.label) {

                            ## Add it to the server
                            Write-Host "`tLoading App Type: " -NoNewline
                            Write-Host "$($AppType.label)" -ForegroundColor DarkBlue
                            $AppType = [TMAppType]::new($AppType.label)
                            New-TMAssetOption -Type 'App Type' -Name $AppType.label -TMSession $TMSession
                        }
                    }
                }
            }

            # Create the Device types
            if ($Components.DeviceTypes) {
                Write-Host 'Loading Device Types...'
                $DeviceTypes = [system.collections.arraylist]@()
                $DeviceTypesFolderPath = Join-Path $ExtensionPath 'DeviceTypes'
                if (Test-Path $DeviceTypesFolderPath -ErrorAction SilentlyContinue) {
                    $DeviceTypeFiles = Get-ChildItem -Path $DeviceTypesFolderPath -ErrorAction SilentlyContinue
                    $DeviceTypeFiles | ForEach-Object {
                        [void]($DeviceTypes.Add((Get-Content -Path $_.FullName | ConvertFrom-Json)))
                    }
                } else {

                    $DeviceTypesFilePath = Join-Path $ExtensionPath 'DeviceTypes.json'
                    if (Test-Path $AppTypesFilePath -ErrorAction SilentlyContinue) {
                        $DeviceTypes = Get-Content -Path $DeviceTypesFilePath | ConvertFrom-Json
                    }
                }

                if ($DeviceTypes.Count -eq 0) {
                    Write-Host "`tNo Device Types to add"
                } else {

                    ## Get the Server's existing Device Types
                    $ServerDeviceTypes = Get-TMAssetOption -Type 'Asset Type' -TMSession $TMSession
                    foreach ($DeviceType in $DeviceTypes) {

                        ## If the server doesn't have the Device type
                        if ($ServerDeviceTypes.label -notcontains $DeviceType.label) {

                            ## Add it to the server
                            Write-Host "`tLoading Device Type: " -NoNewline
                            Write-Host "$($DeviceType.label)" -ForegroundColor DarkBlue
                            $DeviceType = [TMAssetType]::new($DeviceType.label)
                            New-TMAssetOption -Type 'Asset Type' -Name $DeviceType.label -TMSession $TMSession
                        }
                    }
                }
            }

            # Create the Task Categories
            if ($Components.TaskCategories) {
                Write-Host 'Loading Task Categories...'
                $TaskCategories = [system.collections.arraylist]@()
                $TaskCategoriesFolderPath = Join-Path $ExtensionPath 'TaskCategories'
                if (Test-Path $TaskCategoriesFolderPath -ErrorAction SilentlyContinue) {
                    $TaskCategoryFiles = Get-ChildItem -Path $TaskCategoriesFolderPath -ErrorAction SilentlyContinue
                    $TaskCategoryFiles | ForEach-Object {
                        [void]($TaskCategories.Add((Get-Content -Path $_.FullName | ConvertFrom-Json)))
                    }
                } else {
                    $TaskCategoriesFilePath = Join-Path $ExtensionPath 'TaskCategories.json'
                    if (Test-Path $TaskCategoriesFilePath -ErrorAction SilentlyContinue) {
                        $TaskCategories = Get-Content -Path $TaskCategoriesFilePath | ConvertFrom-Json
                    }
                }

                if ($TaskCategories.Count -eq 0) {
                    Write-Host "`tNo Task Categroies to add"
                } else {

                    ## Get the Server's existing Task Categories
                    $ServerTaskCategories = Get-TMAssetOption -Type 'Task Category' -TMSession $TMSession
                    foreach ($TaskCategory in $TaskCategories) {

                        ## If the server doesn't have the Device type
                        if ($ServerTaskCategories.label -notcontains $TaskCategory.label) {

                            ## Add it to the server
                            Write-Host "`tLoading Task Category: " -NoNewline
                            Write-Host "$($TaskCategory.label)" -ForegroundColor DarkBlue
                            $TaskCategory = [TMTaskCategory]::new($TaskCategory.label)
                            New-TMAssetOption -Type 'Task Category' -Name $TaskCategory.label -TMSession $TMSession
                        }
                    }
                }
            }

            # Load the Teams
            if ($Components.Teams) {
                Write-Host 'Loading Teams...'
                $Teams = [system.collections.arraylist]@()
                $TeamsFolderPath = Join-Path $ExtensionPath 'Teams'
                if (Test-Path $TeamsFolderPath -ErrorAction SilentlyContinue) {
                    $TeamFiles = Get-ChildItem -Path $TeamsFolderPath -ErrorAction SilentlyContinue
                    $TeamFiles | ForEach-Object {
                        [void]($Teams.Add((Get-Content -Path $_.FullName | ConvertFrom-Json)))
                    }
                } else {
                    $TeamsFilePath = Join-Path $ExtensionPath 'Teams.json'
                    if (Test-Path $TeamsFilePath -ErrorAction SilentlyContinue) {
                        $Teams = Get-Content -Path $TeamsFilePath | ConvertFrom-Json
                    }
                }

                if ($Teams.Count -eq 0) {
                    Write-Host "`tNo Teams to add"
                } else {
                    foreach ($Team in $Teams) {
                        Write-Host "`tLoading Team: " -NoNewline
                        Write-Host "$($Team.Code)" -ForegroundColor DarkBlue
                        New-TMTeam -Code $Team.Code -Description $Team.Description -Level $Team.Level -Help $Team.Help -TMSession $TMSession
                    }
                }
            }

            # Load the Tags
            if ($Components.Tags) {
                Write-Host 'Loading Tags...'
                $Tags = [system.collections.arraylist]@()
                $TagsFolderPath = Join-Path $ExtensionPath 'Tags'
                if (Test-Path $TagsFolderPath -ErrorAction SilentlyContinue) {
                    $TagFiles = Get-ChildItem -Path $TagsFolderPath -ErrorAction SilentlyContinue
                    $TagFiles | ForEach-Object {
                        [void]($Tags.Add((Get-Content -Path $_.FullName | ConvertFrom-Json)))
                    }
                } else {
                    $TagsFilePath = Join-Path $ExtensionPath 'Tags.json'
                    if (Test-Path $TagsFilePath -ErrorAction SilentlyContinue) {
                        $Tags = Get-Content -Path $TagsFilePath | ConvertFrom-Json
                    }
                }
                if ($Tags.Count -eq 0) {
                    Write-Host "`tNo Tags to add"
                } else {
                    foreach ($Tag in $Tags) {
                        Write-Host "`tLoading Tag: " -NoNewline
                        Write-Host "$($Tag.Name)" -ForegroundColor $TagColorToForeground.($Tag.Color)
                        New-TMTag -Name $Tag.Name -Description $Tag.description -Color $Tag.color -TMSession $TMSession
                    }
                }
            }

            # Load the Field Specs
            if ($Components.FieldSpecs) {
                Write-Host 'Loading Field Specs...'

                ## Check Extension Package v3 (Folder of Files)
                $FieldSpecsFolderPath = Join-Path $ExtensionPath 'FieldSpecs'
                if (Test-Path $FieldSpecsFolderPath -ErrorAction SilentlyContinue) {

                    ## Create the FieldSpecs Object to add all of the fields to
                    $FieldSpecs = [pscustomobject]@{
                        APPLICATION = [PSCustomObject]@{
                            lastUpdated = ''
                            dateCreated = ''
                            domain      = 'APPLICATION'
                            fields      = @()
                            version     = 1
                        }
                        DATABASE    = [PSCustomObject]@{
                            lastUpdated = ''
                            dateCreated = ''
                            domain      = 'DATABASE'
                            fields      = @()
                            version     = 1
                        }
                        DEVICE      = [PSCustomObject]@{
                            lastUpdated = ''
                            dateCreated = ''
                            domain      = 'DEVICE'
                            fields      = @()
                            version     = 1
                        }
                        STORAGE     = [PSCustomObject]@{
                            lastUpdated = ''
                            dateCreated = ''
                            domain      = 'STORAGE'
                            fields      = @()
                            version     = 1
                        }
                    }

                    ## Handle Single JSON field files from folders
                    $SharedFieldFiles = Get-ChildItem -Path (Join-Path $FieldSpecsFolderPath 'Shared') -Filter '*.json' -Force -ErrorAction 'SilentlyContinue'
                    foreach ($FieldFile in $SharedFieldFiles) {
                        $FieldSpecs.APPLICATION.fields += (Get-Content -Path $FieldFile.FullName | ConvertFrom-Json)
                        $FieldSpecs.DATABASE.fields += (Get-Content -Path $FieldFile.FullName | ConvertFrom-Json)
                        $FieldSpecs.DEVICE.fields += (Get-Content -Path $FieldFile.FullName | ConvertFrom-Json)
                        $FieldSpecs.STORAGE.fields += (Get-Content -Path $FieldFile.FullName | ConvertFrom-Json)
                    }

                    $ApplicationFieldFiles = Get-ChildItem -Path (Join-Path $FieldSpecsFolderPath 'Application') -Filter '*.json' -Force -ErrorAction 'SilentlyContinue'
                    foreach ($FieldFile in $ApplicationFieldFiles) {
                        $FieldSpecs.APPLICATION.fields += (Get-Content -Path $FieldFile.FullName | ConvertFrom-Json)
                    }

                    $DatabaseFieldFiles = Get-ChildItem -Path (Join-Path $FieldSpecsFolderPath 'Database') -Filter '*.json' -Force -ErrorAction 'SilentlyContinue'
                    foreach ($FieldFile in $DatabaseFieldFiles) {
                        $FieldSpecs.DATABASE.fields += (Get-Content -Path $FieldFile.FullName | ConvertFrom-Json)
                    }

                    $DeviceFieldFiles = Get-ChildItem -Path (Join-Path $FieldSpecsFolderPath 'Device') -Filter '*.json' -Force -ErrorAction 'SilentlyContinue'
                    foreach ($FieldFile in $DeviceFieldFiles) {
                        $FieldSpecs.DEVICE.fields += (Get-Content -Path $FieldFile.FullName | ConvertFrom-Json)
                    }

                    $StorageFieldFiles = Get-ChildItem -Path (Join-Path $FieldSpecsFolderPath 'Storage') -Filter '*.json' -Force -ErrorAction 'SilentlyContinue'
                    foreach ($FieldFile in $StorageFieldFiles) {
                        $FieldSpecs.STORAGE.fields += (Get-Content -Path $FieldFile.FullName | ConvertFrom-Json)
                    }

                } else {

                    ## Check Extension Package <v3 (Single FieldSpecs.json file)
                    $FieldSpecsFilePath = Join-Path $ExtensionPath 'FieldSpecs.json'
                    if (Test-Path $FieldSpecsFilePath -ErrorAction SilentlyContinue) {

                        ## Version 6.0.2.1+ (and TODO version 6.1?, 5x?)
                        ## Requires the class names to be lower case
                        if ($TMSessionConfig.TMVersion -ge '6.1.0') {
                            ## Convert each Asset Class name to lowercase
                            $FieldSpecs = ((Get-Content -Path $FieldSpecsFilePath) -replace 'constraints', 'constraint')
                        } else {
                            ## No conversion is necessary, assign the provided fields as the updates
                            $FieldSpecs = (((Get-Content -Path $FieldSpecsFilePath) -replace 'constraint', 'constraints') -replace 'constraintss', 'constraints')
                        }
                        $FieldSpecs = $FieldSpecs | ConvertFrom-Json
                    }
                }

                ## Determine if any fields are needed
                if (-Not $FieldSpecs) {
                    Write-Host "`tNo Asset Field Specs to add"
                } else {
                    'APPLICATION', 'DEVICE', 'DATABASE', 'STORAGE' | ForEach-Object {
                        $DomainColor = switch ($_) {
                            'APPLICATION' { 'DarkMagenta' }
                            'DEVICE' { 'DarkCyan' }
                            'DATABASE' { 'DarkYellow' }
                            'STORAGE' { 'DarkGreen' }
                        }
                        foreach ($Field in $FieldSpecs.$_.fields) {
                            Write-Host "`tLoading Field Spec: [" -NoNewline
                            Write-Host "$_" -ForegroundColor $DomainColor -NoNewline
                            Write-Host '] ' -NoNewline
                            Write-Host "$($Field.label)" -ForegroundColor DarkBlue
                        }
                    }
                    Update-TMFieldSpecs -FieldSpecs $FieldSpecs -TMSession $TMSession
                }
            }

            # Load the Bundles
            if ($Components.Bundles) {
                Write-Host 'Loading Bundles...'
                $Bundles = [system.collections.arraylist]@()
                $BundlesFolderPath = Join-Path $ExtensionPath 'Bundles'
                if (Test-Path $BundlesFolderPath -ErrorAction SilentlyContinue) {
                    $BundleFiles = Get-ChildItem -Path $BundlesFolderPath -ErrorAction SilentlyContinue
                    $BundleFiles | ForEach-Object {
                        [void]($Bundles.Add((Get-Content -Path $_.FullName | ConvertFrom-Json)))
                    }
                } else {
                    $BundlesFilePath = Join-Path $ExtensionPath 'Bundles.json'
                    if (Test-Path $BundlesFilePath -ErrorAction SilentlyContinue) {
                        $Bundles = Get-Content -Path $BundlesFilePath | ConvertFrom-Json
                    }
                }
                if ($Bundles.Count -eq 0) {
                    Write-Host "`tNo Bundles to add"
                } else {
                    foreach ($Bundle in $Bundles) {
                        Write-Host "`tLoading Bundle: " -NoNewline
                        Write-Host "$($Bundle.Name)" -ForegroundColor DarkBlue
                        New-TMBundle -Bundle $Bundle -TMSession $TMSession
                    }
                }
            }

            # Load the Events
            if ($Components.Events) {
                Write-Host 'Loading Events...'
                $Events = [system.collections.arraylist]@()
                $EventsFolderPath = Join-Path $ExtensionPath 'Events'
                if (Test-Path $EventsFolderPath -ErrorAction SilentlyContinue) {
                    $EventFiles = Get-ChildItem -Path $EventsFolderPath -ErrorAction SilentlyContinue
                    $EventFiles | ForEach-Object {
                        [void]($Events.Add((Get-Content -Path $_.FullName | ConvertFrom-Json)))
                    }
                } else {
                    $EventsFilePath = Join-Path $ExtensionPath 'Events.json'
                    if (Test-Path $EventsFilePath -ErrorAction SilentlyContinue) {
                        $Events = Get-Content -Path $EventsFilePath | ConvertFrom-Json
                    }
                }
                if ($Events.Count -eq 0) {
                    Write-Host "`tNo Events to add"
                } else {
                    foreach ($TMEvent in $Events) {
                        Write-Host "`tLoading Event: " -NoNewline
                        Write-Host "$($TMEvent.Name)" -ForegroundColor DarkBlue
                        New-TMEvent -InputObject $TMEvent -TMSession $TMSession
                    }
                }
            }

            # Load the Providers
            if ($Components.Providers) {
                Write-Host 'Loading Providers...'
                $Providers = [system.collections.arraylist]@()
                $ProvidersFolderPath = Join-Path $ExtensionPath 'Providers'
                if (Test-Path $ProvidersFolderPath -ErrorAction SilentlyContinue) {
                    $ProviderFiles = Get-ChildItem -Path $ProvidersFolderPath -ErrorAction SilentlyContinue
                    $ProviderFiles | ForEach-Object {
                        [void]($Providers.Add((Get-Content -Path $_.FullName | ConvertFrom-Json)))
                    }
                } else {
                    $ProvidersFilePath = Join-Path $ExtensionPath 'Providers.json'
                    if (Test-Path $ProvidersFilePath -ErrorAction SilentlyContinue) {
                        $Providers = Get-Content -Path $ProvidersFilePath | ConvertFrom-Json
                    }
                }
                if ($Providers.Count -eq 0) {
                    Write-Host "`tNo Providers to add"
                } else {
                    foreach ($Provider in $Providers) {
                        if ($FilterByProvider -and ($Provider.Name -notin $ProviderName)) {
                            continue
                        }
                        Write-Host "`tLoading Provider: " -NoNewline
                        Write-Host "$($Provider.Name)" -ForegroundColor DarkBlue
                        New-TMProvider -Provider $Provider -TMSession $TMSession
                    }
                }
            }

            # Load Actions
            if ($Components.Actions) {
                Write-Host 'Loading Actions...'
                $Actions = @()
                $ActionsPath = Join-Path $ExtensionPath 'Actions'
                if (Test-Path -Path $ActionsPath -ErrorAction SilentlyContinue) {
                    $Actions = Get-ChildItem -Path $ActionsPath -Recurse -Include '*.ps1' -ErrorAction SilentlyContinue | ForEach-Object {
                        Read-TMActionScriptFile -Path $_.FullName -TMSession $TMSession
                    }
                }

                if ($FilterByProvider) {
                    $Actions = $Actions | Where-Object { $_.Provider.Name -in $ProviderName }
                }

                if ($Actions.count -eq 0) {
                    Write-Host "`tNo Actions to add"
                } else {
                    foreach ($Action in $Actions) {
                        Write-Host "`tLoading Action: " -NoNewline
                        Write-Host "$($Action.Name)" -ForegroundColor DarkBlue
                        New-TMAction -Action $Action -Update -TMSession $TMSession
                    }
                }
            }

            # DataScripts
            if ($Components.Datascripts) {
                Write-Host 'Loading DataScripts...'
                $DataScripts = @()
                $DataScriptsPath = Join-Path $ExtensionPath 'DataScripts'
                if (Test-Path -Path $DataScriptsPath -ErrorAction SilentlyContinue) {
                    $DataScripts = Get-ChildItem -Path $DataScriptsPath -Recurse -Include '*.groovy' -ErrorAction SilentlyContinue | ForEach-Object {
                        Read-TMDatascriptFile -Path $_.FullName
                    }
                }

                if ($FilterByProvider) {
                    $DataScripts = $DataScripts | Where-Object { $_.Provider.Name -in $ProviderName }
                }

                if ($DataScripts.count -eq 0) {
                    Write-Host "`tNo DataScripts to add"
                } else {
                    foreach ($DataScript in $DataScripts) {
                        Write-Host "`tLoading DataScript: " -NoNewline
                        Write-Host "$($DataScript.Name)" -ForegroundColor DarkBlue
                        New-TMDatascript -Datascript $DataScript -Update -TMSession $TMSession
                    }
                }
            }

            # Recipes
            if ($Components.Recipes) {
                Write-Host 'Loading Recipes...'
                $Recipes = @()
                $RecipesPath = Join-Path $ExtensionPath 'Recipes'
                if (Test-Path -Path $RecipesPath -ErrorAction SilentlyContinue) {
                    $Recipes = Get-ChildItem -Path $RecipesPath -Recurse -Include '*.json' -ErrorAction SilentlyContinue | ForEach-Object {
                        Read-TMRecipeScriptFile -Path $_.FullName -UpdateFieldNames
                    }
                }

                if ($Recipes.count -eq 0) {
                    Write-Host "`tNo DataScripts to add"
                } else {
                    foreach ($Recipe in $Recipes) {
                        Write-Host "`tLoading Recipe: " -NoNewline
                        Write-Host "$($Recipe.Name)" -ForegroundColor DarkBlue
                        New-TMRecipe -Recipe $Recipe -Update -TMSession $TMSession
                    }
                }
            }

            # Load the AssetViews
            if ($Components.AssetViews) {

                ## Convert any Custom Field Label Tokens from a template
                ## Get a FieldToLabel map to build string replacements
                $AssetViews = [System.Collections.ArrayList]@()
                $LabelReplacements = [System.Collections.ArrayList]@()
                $FieldToLabelMap = Get-TMFieldToLabelMap

                ## Create a replacement token for each asset/label = field set
                foreach ($DomainClass in @('DEVICE', 'APPLICATION', 'DATABASE', 'STORAGE' )) {

                    foreach ($FieldName in $FieldToLabelMap.$DomainClass.Keys) {
                        if ($FieldName -match 'custom\d+') {

                            # Write-Host $FieldName
                            [void]($LabelReplacements.Add([pscustomobject]@{
                                        DomainClass = $DomainClass
                                        FieldLabel  = $FieldToLabelMap.$DomainClass.$FieldName
                                        FieldName   = $FieldName
                                    }
                                )
                            )
                        }
                    }
                }

                $AssetViewsFolderPath = Join-Path $ExtensionPath 'AssetViews'
                $AssetViewsFilePath = Join-Path $ExtensionPath 'AssetViews.json'

                Write-Host 'Loading AssetViews...'
                if (Test-Path $AssetViewsFolderPath -ErrorAction SilentlyContinue) {
                    $AssetViews = Get-ChildItem -Path $AssetViewsFolderPath -File -ErrorAction SilentlyContinue |
                        Where-Object { $_.Extension -in @('.json') } |
                            ForEach-Object {
                                Get-Content -Path $_.FullName | ConvertFrom-Json
                            }
                } else {
                    if (-Not (Test-Path $AssetViewsFilePath -ErrorAction SilentlyContinue)) {
                        Write-Host "`tNo AssetViews to add"
                    } else {
                        $AssetViews = Get-Content -Path $AssetViewsFilePath | ConvertFrom-Json
                        if ($AssetViews.Count -eq 0) { Write-Host "`tNo Asset Views to add" }
                    }
                }
                foreach ($AssetView in $AssetViews) {
                    Write-Host "`tLoading AssetView: " -NoNewline
                    Write-Host "$($AssetView.Name)" -ForegroundColor DarkBlue

                    ## Replace all 'customN' items with a codestring to replace the correct items in the offline package
                    $SourceCodeLines = ($AssetView | ConvertTo-Json -Depth 100) -split "`r`n" -split "`r" -split "`n"
                    $UpdatedSourceCodeLines = [System.Collections.ArrayList]::new()

                    foreach ($SourceCodeLine in $SourceCodeLines) {
                        $CurrentLine = $SourceCodeLine
                        $LabelReplacements | ForEach-Object {
                            $ReplaceString = "customN\|$($_.DomainClass)\|$($_.FieldLabel)\|"
                            if ($CurrentLine -match $ReplaceString) {
                                $CurrentLine = $CurrentLine -replace $ReplaceString, $_.FieldName
                            }
                        }
                        [void]($UpdatedSourceCodeLines.Add($CurrentLine))
                    }
                    $AssetView = $UpdatedSourceCodeLines -Join "`n" | ConvertFrom-Json

                    New-TMAssetViewConfiguration $AssetView -TMSession $TMSession -Update
                }
            }

            # SetupScripts
            if ($Components.SetupScripts) {
                Write-Host 'Collecting Setup Scripts...'
                $SetupScriptsFolderPath = Join-Path $ExtensionPath '_SETUP'
                if (-Not (Test-Path $SetupScriptsFolderPath -ErrorAction SilentlyContinue)) {
                    Write-Host "`tNo Setup Scripts to run"
                } else {
                    $SetupScriptFiles = Get-ChildItem -Path $SetupScriptsFolderPath -File -ErrorAction SilentlyContinue | Where-Object { $_.Extension -eq '.ps1' }
                    foreach ($SetupScriptFile in $SetupScriptFiles) {
                        Write-Host "`Queuing Setup Script: " -NoNewline
                        Write-Host "$($SetupScriptFile.Name)" -ForegroundColor DarkBlue
                        [void]$SetupScriptsToRun.Add($SetupScriptFile)
                    }
                }
            }
        }

        ## Sort the Setup Scripts to run by name
        $SortedSetupScripts = $SetupScriptsToRun | Sort-Object -Property 'Name'

        ## Iterate over each setup script and run them each
        foreach ($SetupScript in $SortedSetupScripts) {

            Write-Host "`tRunning Setup Script: " -NoNewline
            Write-Host "$($SetupScript.Name)" -ForegroundColor DarkBlue

            ## Try the script
            try {

                & "$($SetupScript.FullName)"
            } catch {
                Write-Host 'Setup Script Failed!'
                Write-Error $_
            }
        }
    }
}

function Export-TMExtension {
    <#
    .SYNOPSIS
    Exports a TransitionManager project into a portable configuration package
    that can be applied in a different project and/or on a different server

    .DESCRIPTION
    This function will retreieve the different configuration components from a
    TransitonManager project and save them locally in a standardized format
    that can then be used to import those configurations on another TM project
    or TM instance.

    .PARAMETER Name
    What the Reference Design will be named

    .PARAMETER TMSession
    The name of the TM Session to use when creating a Team

    .PARAMETER Project
    The project that will be exported

    .PARAMETER Include
    An array of components to include when performing the export.
    Options are:
        *
        ServerSettings
        ProjectSettings
        Code
        DependencyTypes
        FieldSpecs
        Bundles
        Providers
        Actions
        DataScripts
        Recipes
        AssetViews
        Events
        AppTypes
        DeviceTypes
        Teams
        TaskCategories
    NOTE: ServerSettings, ProjectSettings and Code are not components, but sets of components that are expanded to the following during runtime:
        ServerSettings: App Types, Dependency Types, Device Types, Teams, and Task Categories
        ProjectSettings: Asset Views, Bundles, Events, FieldSpecs, Providers, and Tags
        Code: Actions, DataScripts, Recipes, and (On Import Only) SetupScripts.
    .PARAMETER Exclude
    An array of components to skip when performing the export.
    Options are:
        ServerSettings
        ProjectSettings
        Code
        DependencyTypes
        FieldSpecs
        Bundles
        Providers
        Actions
        DataScripts
        Recipes
        AssetViews
        Events
        AppTypes
        DeviceTypes
        Teams
        TaskCategories
    NOTE: ServerSettings, ProjectSettings and Code are not components, but sets of components that are expanded to the following during runtime:
        ServerSettings: App Types, Dependency Types, Device Types, Teams, and Task Categories
        ProjectSettings: Asset Views, Bundles, Events, FieldSpecs, Providers, and Tags
        Code: Actions, DataScripts, Recipes, and (On Import Only) SetupScripts.
    .PARAMETER OutPath
    The directory where the Reference Design will be saved

    .EXAMPLE
    $Credential = Get-StoredCredential -Name 'ME'
    $ExportSplat = @{
        Name = 'VMWare - vCenter'
        Server = 'tmddev.transitionmanager.net'
        Project = 'Extension - VMware vCenter'
        Credential = $Credential
    }
    Export-TMExtension @ExportSplat

    .OUTPUTS
    None
    #>


    [alias('Export-TMExtension')]
    [CmdletBinding(DefaultParameterSetName = 'ByInclusion')]
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [String]$Name,

        [Parameter(Mandatory = $false, Position = 1)]
        [String]$TMSession = 'Default',

        [Parameter(Mandatory = $false, Position = 2)]
        [String]$Project,

        [Parameter(Mandatory = $false, Position = 3, ParameterSetName = 'ByInclusion')]
        [ValidateSet(
            '*',
            'ServerSettings',
            'ProjectSettings',
            'Code',
            'DependencyTypes',
            'FieldSpecs',
            'Bundles',
            'Providers',
            'Actions',
            'DataScripts',
            'Recipes',
            'Tags',
            'AssetData',
            'AssetViews',
            'Events',
            'AppTypes',
            'DeviceTypes',
            'Teams',
            'TaskCategories'
        )]
        [String[]]$Include = '*',

        [Parameter(Mandatory = $false, Position = 3, ParameterSetName = 'ByExclusion')]
        [ValidateSet(
            'ServerSettings',
            'ProjectSettings',
            'Code',
            'DependencyTypes',
            'FieldSpecs',
            'Bundles',
            'Providers',
            'Actions',
            'DataScripts',
            'Recipes',
            'Tags',
            'AssetData',
            'AssetViews',
            'Events',
            'AppTypes',
            'DeviceTypes',
            'Teams',
            'TaskCategories'
        )]
        [String[]]$Exclude,

        [Parameter(Mandatory = $false, Position = 4)]
        [String[]]$ProviderName,

        [Parameter(Mandatory = $false, Position = 5)]
        [AllowNull()]
        [String]$OutPath = '',

        [Parameter(Mandatory = $false, Position = 6)]
        [Switch]$ClearExistingFiles
    )

    begin {
        # As per TM-22256, ServerSettings, ProjectSettings, and Code will be used to contain a set of components to exclude/include.
        $ServerSettings = 'AppTypes',
        'DependencyTypes',
        'DeviceTypes',
        'Teams',
        'TaskCategories'
        $ProjectSettings = 'AssetViews',
        'Bundles',
        'Events',
        'FieldSpecs',
        'Providers',
        'Tags'
        $Code = 'Actions',
        'DataScripts',
        'Recipes'
        # (On Import Only)
        # 'SetupScripts'
        if ($Include -contains 'ServerSettings') {$Include += $ServerSettings}
        if ($Include -contains 'ProjectSettings') {$Include += $ProjectSettings}
        if ($Include -contains 'Code') {$Include += $Code}
        if ($Exclude -contains 'ServerSettings') {$Exclude += $ServerSettings}
        if ($Exclude -contains 'ProjectSettings') {$Exclude += $ProjectSettings}
        if ($Exclude -contains 'Code') {$Exclude += $Code}

        # Get the session configuration
        Write-Verbose 'Checking for cached TMSession'
        $TMSessionConfig = $global:TMSessions[$TMSession]
        Write-Debug 'TMSessionConfig:'
        Write-Debug ($TMSessionConfig | ConvertTo-Json -Depth 5)
        if (-not $TMSessionConfig) {
            throw "TMSession '$TMSession' not found. Use New-TMSession command before using features."
        }

        if (!(Get-Module 'TMD.Common')) {
            Import-Module 'TMD.Common'
        }

        # Default to the reference design directory if an out path wasn't specified
        if ([String]::IsNullOrWhiteSpace($OutPath)) {
            Write-Verbose 'No OutPath as provided and will use the default export location'
            $OutPath = Join-Path $Global:userPaths.referencedesigns 'Exported' $TMSessionConfig.TMServer
        }

        # Set up a configuration object to determine which components to install later in the process block
        $Components = @{
            DependencyTypes = $true
            FieldSpecs      = $true
            Bundles         = $true
            Providers       = $true
            Actions         = $true
            DataScripts     = $true
            Recipes         = $true
            Tags            = $true
            AssetData       = $true
            AssetViews      = $true
            Events          = $true
            AppTypes        = $true
            DeviceTypes     = $true
            Teams           = $true
            TaskCategories  = $true
        }

        switch ($PSCmdlet.ParameterSetName) {
            'ByInclusion' {
                if ($Include -notcontains '*') {
                    foreach ($Key in $Components.GetEnumerator().Name) {
                        if ($Include -notcontains $Key) {
                            $Components.$Key = $false
                        }
                    }
                }
            }

            'ByExclusion' {
                # Process the Exclude parameter and turn components off
                $ComponentSets = 'ServerSettings', 'ProjectSettings', 'Code'
                $Exclude | ForEach-Object {
                    #Component sets are not actual components, so we don't want to process them.
                    if ($_ -notin $ComponentSets) {
                        $Components.$_ = $false
                    }
                }
            }
        }

        # Write about what is going to be exported
        Write-Host 'Exporting Component Types from Project:'
        foreach ($Component in $Components.Keys) {

            ## For each Enabled Component
            if ($Components.$Component) {
                Write-Host "`t$Component" -ForegroundColor Cyan
            }
        }

        # Determine the project to export
        if (-not [String]::IsNullOrWhiteSpace($Project) -and ($TMSessionConfig.userContext.project.name -ne $Project)) {
            Enter-TMProject -ProjectName $Project -TMSession $TMSession
        } else {
            $Project = $TMSessionConfig.userContext.project.name
        }

        ## Set the name to the project name if it wasn't provided
        $Name = [String]::IsNullOrWhiteSpace($Name) ? $Project : $Name

        ## Write a Banner
        Write-Host 'Exporting TM Extension From:'
        Write-Host "`tServer: " -NoNewline
        Write-Host $TMSessionConfig.TMServer -ForegroundColor Cyan
        Write-Host "`tProject: " -NoNewline
        Write-Host $Project -ForegroundColor Cyan
        Write-Host "`tAt: " -NoNewline
        Write-Host (Get-Date).ToString() -ForegroundColor Cyan
        Write-Host "`tTo: " -NoNewline
        Write-Host $OutPath -ForegroundColor Cyan

        $FilterByProvider = $false
        Write-Host "`tProviders:`t" -NoNewline
        if ($ProviderName) {
            Write-Host $ProviderName -ForegroundColor Cyan
            $FilterByProvider = $true
        } else {
            Write-Host 'All Providers' -ForegroundColor Cyan
        }

        ## Clear existing files from the output folder
        if ($ClearExistingFiles) {
            Write-Host 'Export is Clearing Existing files before writing!!' -ForegroundColor Magenta

            ## Delete Files
            $ExistingFiles = Get-ChildItem -Path (Join-Path $OutPath $Name '_REFERENCE DESIGN') -Recurse -File -Force -ErrorAction SilentlyContinue
            foreach ($ExistingFile in $ExistingFiles) {
                Write-Verbose "Removing Old File: $($ExistingFile.Name)"
                Remove-Item $ExistingFile -Force -Recurse
            }

            ## Delete Folders
            $ExistingFolders = Get-ChildItem -Path (Join-Path $OutPath $Name '_REFERENCE DESIGN') -Recurse -Directory -Force -ErrorAction SilentlyContinue
            foreach ($ExistingFolder in $ExistingFolders) {
                Write-Verbose "Removing Old File: $($ExistingFolder.Name)"
                Remove-Item $ExistingFolder -Force -Recurse -ErrorAction 'SilentlyContinue'
            }
        }
    }

    process {
        # Set up the directory structure for all of the output files
        $OutputDirectory = Join-Path $OutPath $Name
        $ExtensionDirectory = Join-Path $OutputDirectory '_REFERENCE DESIGN'
        $ExtensionDataDirectory = Join-Path $OutputDirectory '_REFERENCE DATA'

        Test-FolderPath $ExtensionDirectory
        Test-FolderPath (Join-Path $OutputDirectory 'Documentation')
        Test-FolderPath (Join-Path $OutputDirectory '_REFERENCE DATA')

        ##
        ## Export all of the components
        ##

        # Export the Dependency Types
        if ($Components.DependencyTypes) {
            Write-Host 'Exporting Dependency Types...' -NoNewline
            $DependencyTypesFolderPath = Join-Path $ExtensionDirectory 'DependencyTypes'
            Test-FolderPath -FolderPath $DependencyTypesFolderPath

            $DependencyTypes = Get-TMDependencyType -ResetIDs -TMSession $TMSession
            if (!$DependencyTypes) {
                Write-Host "`tNo Dependency Types to export"
            } else {
                $DependencyTypes | ForEach-Object {
                    if (-Not [string]::IsNullOrWhiteSpace(${_.label}?.Trim())) {
                        $Filename = Get-FilenameSafeString -String $_.label
                        $_ | ConvertTo-Json -Depth 100 | Set-Content -Path (Join-Path $DependencyTypesFolderPath "$Filename.json") -Force
                    }
                }
                Write-Host "`t$($DependencyTypes.Count) " -NoNewline -ForegroundColor DarkBlue
                Write-Host 'Dependency Type(s) exported'
            }
        }

        # Export the App Types
        if ($Components.AppTypes) {
            Write-Host "Exporting App Types...`t" -NoNewline
            $AppTypesFolderPath = Join-Path $ExtensionDirectory 'AppTypes'
            Test-FolderPath -FolderPath $AppTypesFolderPath

            $AppTypes = Get-TMAssetOption -Type 'App Type' -ResetIDs -TMSession $TMSession
            if (!$AppTypes) {
                Write-Host "`tNo App Types to export"
            } else {
                $AppTypes | ForEach-Object {
                    if (-Not [string]::IsNullOrWhiteSpace(${_.label}?.Trim())) {
                        $Filename = Get-FilenameSafeString -String $_.label
                        $_ | ConvertTo-Json -Depth 100 | Set-Content -Path (Join-Path $AppTypesFolderPath "$Filename.json") -Force
                    }
                }
                Write-Host "`t$($AppTypes.Count) " -NoNewline -ForegroundColor DarkBlue
                Write-Host 'App Type(s) exported'
            }
        }

        # Export the Device Types
        if ($Components.DeviceTypes) {
            Write-Host 'Exporting Device Types...' -NoNewline
            $DeviceTypesFolderPath = Join-Path $ExtensionDirectory 'DeviceTypes'
            Test-FolderPath -FolderPath $DeviceTypesFolderPath

            $DeviceTypes = Get-TMAssetOption -Type 'Asset Type' -ResetIDs -TMSession $TMSession
            if (!$DeviceTypes) {
                Write-Host "`tNo Device Types to export"
            } else {
                $DeviceTypes | ForEach-Object {
                    if (-Not [string]::IsNullOrWhiteSpace(${_.label}?.Trim())) {
                        $Filename = Get-FilenameSafeString -String $_.label
                        $_ | ConvertTo-Json -Depth 100 | Set-Content -Path (Join-Path $DeviceTypesFolderPath "$Filename.json") -Force
                    }
                }
                Write-Host "`t$($DeviceTypes.Count) " -NoNewline -ForegroundColor DarkBlue
                Write-Host 'Device Type(s) exported'
            }
        }

        # Export the Task Categories
        if ($Components.TaskCategories) {
            Write-Host 'Exporting Task Categories...' -NoNewline
            $TaskCategoriesFolderPath = Join-Path $ExtensionDirectory 'TaskCategories'
            Test-FolderPath -FolderPath $TaskCategoriesFolderPath

            $TaskCategories = Get-TMAssetOption -Type 'Task Category' -ResetIDs -TMSession $TMSession
            if (!$TaskCategories) {
                Write-Host "`tNo Task Categories to export"
            } else {
                $TaskCategories | ForEach-Object {
                    if (-Not [string]::IsNullOrWhiteSpace(${_.label}?.Trim())) {
                        $Filename = Get-FilenameSafeString -String $_.label
                        $_ | ConvertTo-Json -Depth 100 | Set-Content -Path (Join-Path $TaskCategoriesFolderPath "$Filename.json") -Force
                    }
                }
                Write-Host "`t$($TaskCategories.Count) " -NoNewline -ForegroundColor DarkBlue
                Write-Host 'Task Categories exported'
            }
        }

        # Export the Field Specs
        if ($Components.FieldSpecs) {
            Write-Host 'Exporting Field Specs...' -NoNewline
            $FieldSpecs = Get-TMFieldSpecs -ResetIDs -CustomOnly -TMSession $TMSession
            if (!$FieldSpecs) {
                Write-Host "`tNo Asset Field Specs to export"
            } else {
                $FieldSpecsFolderPath = Join-Path $ExtensionDirectory 'FieldSpecs'
                Test-FolderPath -Path (Join-Path $FieldSpecsFolderPath 'Shared')
                Test-FolderPath -Path (Join-Path $FieldSpecsFolderPath 'Application')
                Test-FolderPath -Path (Join-Path $FieldSpecsFolderPath 'Database')
                Test-FolderPath -Path (Join-Path $FieldSpecsFolderPath 'Device')
                Test-FolderPath -Path (Join-Path $FieldSpecsFolderPath 'Storage')

                ## Iterate through each Domain Class
                ##### This is done in this order expressly expressly because shared fields will be UPDATED because it exists in each domain.
                ##### This order ensures that if there is variation in the color/order, etc. Application will win.
                ##### This is because the Shared fields are inserted with ONE configuration across each domain.
                $DomainClasses = @('STORAGE', 'DATABASE', 'DEVICE', 'APPLICATION')
                foreach ($DomainClass in $DomainClasses) {

                    ## Iterate over each Domain Field
                    $DomainFields = $FieldSpecs.$DomainClass.fields
                    foreach ($DomainField in $DomainFields) {

                        $FieldFileName = Get-FilenameSafeString "$($DomainField.order)-$($DomainField.label)"
                        $FieldFilePath = Join-Path $FieldSpecsFolderPath ($DomainField.shared -eq 1 ? 'Shared' : $DomainClass) ($FieldFileName + '.json')
                        Set-Content -Path $FieldFilePath -Value ($DomainField | ConvertTo-Json -Depth 5) -Force
                    }
                }
                Write-Host "`tAsset Field Specs exported"
            }
        }

        # Export the Bundles
        if ($Components.Bundles) {
            Write-Host "Exporting Bundles...`t" -NoNewline
            $BundlesFolderPath = Join-Path $ExtensionDirectory 'Bundles'
            Test-FolderPath -FolderPath $BundlesFolderPath

            $Bundles = Get-TMBundle -ResetIDs -TMSession $TMSession
            if (!$Bundles) {
                Write-Host "`tNo Bundles to export"
            } else {
                $Bundles | ForEach-Object {
                    $Filename = Get-FilenameSafeString -String $_.Name
                    $_ | ConvertTo-Json -Depth 100 | Set-Content -Path (Join-Path $BundlesFolderPath "$Filename.json") -Force
                }
                Write-Host "`t$($Bundles.Count) " -NoNewline -ForegroundColor DarkBlue
                Write-Host 'Bundle(s) exported'
            }
        }

        # Export the Events
        if ($Components.Events) {
            Write-Host "Exporting Events...`t" -NoNewline
            $EventsFolderPath = Join-Path $ExtensionDirectory 'Events'
            Test-FolderPath -FolderPath $EventsFolderPath

            $Events = Get-TMEvent -ResetIDs -TMSession $TMSession
            if (!$Events) {
                Write-Host "`tNo Events to export"
            } else {
                $Events | ForEach-Object {
                    $Filename = Get-FilenameSafeString -String $_.Name
                    $_ | ConvertTo-Json -Depth 100 | Set-Content -Path (Join-Path $EventsFolderPath "$Filename.json") -Force
                }
                Write-Host "`t$($Events.Count) " -NoNewline -ForegroundColor DarkBlue
                Write-Host 'Event(s) exported'
            }
        }

        # Export the Teams
        if ($Components.Teams) {
            Write-Host "Exporting Teams...`t" -NoNewline
            $TeamsFolderPath = Join-Path $ExtensionDirectory 'Teams'
            Test-FolderPath -FolderPath $TeamsFolderPath

            $Teams = Get-TMTeam -TMSession $TMSession
            if (!$Teams) {
                Write-Host "`tNo Teams to export"
            } else {
                $Teams | ForEach-Object {
                    $Filename = Get-FilenameSafeString -String $_.Code
                    $_ | ConvertTo-Json -Depth 100 | Set-Content -Path (Join-Path $TeamsFolderPath "$Filename.json") -Force
                }
                Write-Host "`t$($Teams.Count) " -NoNewline -ForegroundColor DarkBlue
                Write-Host 'Team(s) exported'
            }
        }

        # Export the Providers
        $Providers = Get-TMProvider -ResetIDs -TMSession $TMSession
        if (-not $FilterByProvider) {
            $ProviderName = $Providers.Name
        }
        if ($Components.Providers) {
            Write-Host "Exporting Providers...`t" -NoNewline
            $ProvidersFolderPath = Join-Path $ExtensionDirectory 'Providers'
            Test-FolderPath -FolderPath $ProvidersFolderPath

            if (!$Providers) {
                Write-Host "`tNo Providers to export"
            } else {
                if ($FilterByProvider) {
                    $Providers = $Providers | Where-Object Name -In $ProviderName
                }
                $Providers | ForEach-Object {
                    $Filename = Get-FilenameSafeString -String $_.Name
                    $_ | ConvertTo-Json -Depth 100 | Set-Content -Path (Join-Path $ProvidersFolderPath "$Filename.json") -Force
                }
                Write-Host "`t$($Providers.Count) " -NoNewline -ForegroundColor DarkBlue
                Write-Host 'Provider(s) exported'
            }
        }

        # Export the Actions
        if ($Components.Actions) {

            Write-Host "Exporting Actions...`t" -NoNewline
            $ActionsFolderPath = Join-Path $ExtensionDirectory 'Actions'
            Test-FolderPath $ActionsFolderPath
            $Actions = Get-TMAction -SaveCodePath $ActionsFolderPath -ProviderName $ProviderName -ResetIDs -Passthru -TMSession $TMSession
            if (!$Actions) {
                Write-Host "`tNo Actions to export"
            } else {
                Write-Host "`t$($Actions.Count) " -NoNewline -ForegroundColor DarkBlue
                Write-Host 'Action(s) exported'
            }
        }

        # Export the DataScript Scripts
        if ($Components.DataScripts) {

            Write-Host 'Exporting DataScripts...' -NoNewline
            $DataScriptsFolderPath = Join-Path $ExtensionDirectory 'DataScripts'
            Test-FolderPath $DataScriptsFolderPath
            $DataScripts = Get-TMDatascript -ResetIDs -SaveCodePath $DataScriptsFolderPath -ProviderName $ProviderName -Passthru -TMSession $TMSession
            if (!$DataScripts) {
                Write-Host "`tNo Datascripts to export"
            } else {
                # TODO: Is the 'Private' folder supposed to be excluded?
                Write-Host "`t$($DataScripts.Count) " -NoNewline -ForegroundColor DarkBlue
                Write-Host 'Datascript(s) exported'
            }
        }

        # Export the Recipes
        if ($Components.Recipes) {

            Write-Host "Exporting Recipes...`t" -NoNewline
            $RecipesFolderPath = Join-Path $ExtensionDirectory 'Recipes'
            Test-FolderPath $RecipesFolderPath
            $Recipes = Get-TMRecipe -ResetIDs -SaveCodePath $RecipesFolderPath -Passthru -TMSession $TMSession
            if (!$Recipes) {
                Write-Host "`tNo Recipes to export"
            } else {
                Write-Host "`t$($Recipes.Count) " -NoNewline -ForegroundColor DarkBlue
                Write-Host 'Recipe(s) exported'
            }
        }

        # Export the Tags
        if ($Components.Tags) {
            Write-Host "Exporting Tags...`t" -NoNewline
            $TagsFolderPath = Join-Path $ExtensionDirectory 'Tags'
            Test-FolderPath -FolderPath $TagsFolderPath

            $Tags = Get-TMTag -ResetIDs -TMSession $TMSession
            if (!$Tags) {
                Write-Host "`tNo Tags to export"
            } else {
                $Tags | ForEach-Object {
                    $Filename = Get-FilenameSafeString -String $_.Name
                    $_ | ConvertTo-Json -Depth 100 | Set-Content -Path (Join-Path $TagsFolderPath "$Filename.json") -Force
                }
                Write-Host "`t$($Tags.Count) " -NoNewline -ForegroundColor DarkBlue
                Write-Host 'Tags(s) exported'
            }
        }

        # Export the AssetViews
        if ($Components.AssetViews) {
            Write-Host "Exporting AssetViews...`t" -NoNewline
            $AssetViewsFolderPath = Join-Path $ExtensionDirectory 'AssetViews'
            Test-FolderPath -FolderPath $AssetViewsFolderPath

            $AssetViews = Get-TMAssetViewConfiguration -ResetIDs -TMSession $TMSession -ExcludeSystem
            if (!$AssetViews) {
                Write-Host "`tNo AssetViews to export"
            } else {

                ## Get a FieldToLabel map to build string replacements
                $LabelReplacements = [System.Collections.ArrayList]@()
                $FieldToLabelMap = Get-TMFieldToLabelMap

                ## Create a replacement token for each asset/label = field set
                foreach ($DomainClass in @('DEVICE', 'APPLICATION', 'DATABASE', 'STORAGE' )) {

                    foreach ($FieldName in $FieldToLabelMap.$DomainClass.Keys) {
                        if ($FieldName -match 'custom\d+') {

                            # Write-Host $FieldName
                            [void]($LabelReplacements.Add([pscustomobject]@{
                                        DomainClass = $DomainClass
                                        FieldLabel  = $FieldToLabelMap.$DomainClass.$FieldName
                                        FieldName   = $FieldName
                                    }
                                )
                            )
                        }
                    }
                }

                $AssetViews | ForEach-Object {
                    $AssetViewName = $_.name

                    ## Replace all customN in the JSON Data
                    $SourceCodeLines = ($_ | ConvertTo-Json -Depth 10) -split "`n"
                    $UpdatedSourceCodeLines = [System.Collections.ArrayList]::new()
                    foreach ($SourceCodeLine in $SourceCodeLines) {
                        $CurrentLine = $SourceCodeLine
                        $LabelReplacements | ForEach-Object {
                            if ($CurrentLine -match $_.FieldName) {
                                $CurrentLine = $CurrentLine -replace $_.FieldName, "customN|$($_.DomainClass)|$($_.FieldLabel)|"
                            }
                        }
                        [void]($UpdatedSourceCodeLines.Add($CurrentLine))
                    }
                    Test-FolderPath -FolderPath $AssetViewsFolderPath
                    $AssetViewFileName = Get-FilenameSafeString $AssetViewName
                    $UpdatedSourceCodeLines -join "`n" | Set-Content -Path (Join-Path $AssetViewsFolderPath "$AssetViewFileName.json") -Force
                }
                Write-Host "`t$($AssetViews.Count) " -NoNewline -ForegroundColor DarkBlue
                Write-Host 'AssetView(s) exported'
            }
        }

        # Export Asset Data
        if ($Components.AssetData) {
            Write-Host 'Exporting Asset Data...' -NoNewline
            Export-TMExcel -Path $ExtensionDataDirectory -TMSession $TMSession
            Write-Host "`t`tAsset Data exported"
        }

        Write-Host 'Creating package.json file...'
        New-TMExtensionPackageFile -Name $Name -Path $OutputDirectory
    }
}

function New-TMExtensionPackageFile {
    <#
    .SYNOPSIS
    Creates a new package.json file for a locally downloaded/exported Reference Design

    .DESCRIPTION
    This function will look at the

    .PARAMETER Name
    Parameter description

    .PARAMETER Path
    Parameter description

    .PARAMETER Description
    Parameter description

    .PARAMETER Author
    Parameter description

    .PARAMETER Email
    Parameter description

    .PARAMETER Passthru
    A switch indicating that the created package object should be sent back
    to the caller

    .EXAMPLE
    New-TMExtensionPackageFile -Name 'TM - Rules Engine' -Path 'C:\DEV\Downloaded Reference Design'

    .EXAMPLE
    $PackageSplat = @{
        Name = VMware - vCenter'
        Path = 'C:\Users\user\TMD_Files\Reference Designs\Downloaded\VMware - vCenter'
        Description = 'This is a detailed description of the Reference Design'
        Author = 'Me'
        Email = 'me@tdsi.com'
    }
    New-TMExtensionPackageFile @PackageSplat

    .EXAMPLE
    $ExtensionPackage = New-TMExtensionPackageFile -Name 'New Extension' -Path 'C:\DEV\Extension' -Passthru
    Write-Host "There are $($ExtensionPackage.Inventory.actions.Count) action(s) included in this Reference Design"

    .OUTPUTS
    A [PSCustomObject] if -Passthru is specified, otherwise nothing
    #>


    [alias('New-TMReferenceDesignPackageFile')]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [String]$Name,

        [Parameter(Mandatory = $true, Position = 1)]
        [ValidateScript( { (Get-Item $_) -is [System.IO.DirectoryInfo] })]
        [String]$Path,

        [Parameter(Mandatory = $false, Position = 2)]
        [String]$Description,

        [Parameter(Mandatory = $false, Position = 3)]
        [String]$Author,

        [Parameter(Mandatory = $false, Position = 4)]
        [String]$Email,

        [Parameter(Mandatory = $false)]
        [Switch]$Passthru

    )

    $Inventory = @{
        Artifacts       = [System.Collections.ArrayList]::new()
        ArtifactLinks   = [System.Collections.ArrayList]::new()
        Actions         = [System.Collections.ArrayList]::new()
        DataScripts     = [System.Collections.ArrayList]::new()
        Recipes         = [System.Collections.ArrayList]::new()
        Bundles         = [System.Collections.ArrayList]::new()
        Providers       = [System.Collections.ArrayList]::new()
        DependencyTypes = [System.Collections.ArrayList]::new()
        FieldSpecs      = @{}
    }
    $ExtensionPath = Join-Path $Path '_REFERENCE DESIGN'

    ## Read the Actions Folder to get the name of Scripts
    $ActionFiles = Get-ChildItem -Path (Join-Path $ExtensionPath 'Actions') -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -like '*.ps1' }
    foreach ($ActionFile in $ActionFiles) {

        ## Convert the Script into a proper ActionObject
        $Action = Read-TMActionScriptFile -Path $ActionFile.FullName -TMSession $TMSession

        ## Some files are not setup or created with all of the metadata. Fill in details based on the filename.
        if (-Not $Action.Name) {

            ## Add this Action to the Actions array for writing to JSON
            [void]$Inventory.Actions.Add(
                [PSCustomObject]@{
                    name        = $ActionFile.BaseName
                    description = ''
                }
            )
        } else {

            ## Add this Action to the Actions array for writing to JSON
            [void]$Inventory.Actions.Add(
                [PSCustomObject]@{
                    name        = $Action.Name
                    description = $Action.Description
                }
            )
        }
    }

    ## Read the DataScript Scripts Folder to get the name Scripts
    if (Test-Path($ExtensionPath + '\DataScripts')) {
        $DataScriptFiles = Get-ChildItem -Path (Join-Path $ExtensionPath 'DataScripts') -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -like '*.groovy' }
    } elseif (Test-Path($ExtensionPath + '\ETL')) {
        $DataScriptFiles = Get-ChildItem -Path (Join-Path $ExtensionPath 'ETL') -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -like '*.groovy' }
    }

    foreach ($DataScriptFile in $DataScriptFiles) {

        ## Convert the Script into a proper TMDatascript Object
        $DataScript_File = Read-TMDatascriptFile -Path $DataScriptFiles.FullName -ErrorAction 'SilentlyContinue'

        ## Some files are not setup or created with all of the metadata. Fill in details based on the filename.
        if (-Not $DataScript_File) {
            [void]$Inventory.DataScripts.Add(
                [PSCustomObject]@{
                    name        = $DataScript_File.name
                    description = ''
                }
            )
        } else {

            ## Add this DataScript Script to the array for writing to JSON
            [void]$Inventory.DataScripts.Add(
                [PSCustomObject]@{
                    name        = $DataScript_File.name
                    description = $DataScript_File.description
                }
            )
        }
    }

    ## Read the Recipes Folder to get the name Scripts
    $RecipeFiles = Get-ChildItem -Path (Join-Path $ExtensionPath 'Recipes') -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.Name -like '*.json' }
    foreach ($RecipeFile in $RecipeFiles) {

        ## Convert the Script into a proper TMRecipeScript Object
        $Recipe = Read-TMRecipeScriptFile -Path $RecipeFile.FullName

        ## Some files are not setup or created with all of the metadata. Fill in details based on the filename.
        if (-Not $Recipe) {
            ## Add this Recipe Script to the array for writing to JSON
            [void]$Inventory.Recipes.Add(
                [PSCustomObject]@{
                    name        = $RecipeFile.name
                    description = $RecipeFile.description
                }
            )
        } else {

            ## Add this Recipe Script to the array for writing to JSON
            [void]$Inventory.Recipes.Add(
                [PSCustomObject]@{
                    name        = $Recipe.name
                    description = $Recipe.description
                }
            )
        }
    }

    # Get the bundles to add to the inventory
    $BundleFile = Join-Path $ExtensionPath 'Bundles.json'
    if (Test-Path -Path $BundleFile) {
        Get-Content -Path $BundleFile | ConvertFrom-Json | ForEach-Object {
            [void]$Inventory.Bundles.Add(
                [PSCustomObject]@{
                    name        = $_.name
                    description = $_.description
                }
            )
        }
    }

    # Get the providers to add to the inventory
    $ProvidersFile = Join-Path $ExtensionPath 'Providers.json'
    if (Test-Path $ProvidersFile -ErrorAction SilentlyContinue) {
        Get-Content -Path $ProvidersFile | ConvertFrom-Json | ForEach-Object {
            [void]$Inventory.Providers.Add(
                [PSCustomObject]@{
                    name        = $_.name
                    description = $_.description
                }
            )
        }
    }

    # Get the Dependency Types to add to the inventory
    $DependencyTypesFile = Join-Path $ExtensionPath 'DependencyTypes.json'
    if (Test-Path $DependencyTypesFile -ErrorAction SilentlyContinue) {
        Get-Content -Path $DependencyTypesFile | ConvertFrom-Json | ForEach-Object {
            [void]$Inventory.DependencyTypes.Add($_.label)
        }
    }

    ## Check Extension Package v3 (Folder of Files)
    $FieldSpecsFolderPath = Join-Path $ExtensionPath 'FieldSpecs'
    if (Test-Path $FieldSpecsFolderPath -ErrorAction SilentlyContinue) {

        ## Create the FieldSpecs Object to add all of the fields to
        $FieldSpecs = [pscustomobject]@{
            APPLICATION = [PSCustomObject]@{
                lastUpdated = ''
                dateCreated = ''
                domain      = 'APPLICATION'
                fields      = @()
                version     = 1
            }
            DATABASE    = [PSCustomObject]@{
                lastUpdated = ''
                dateCreated = ''
                domain      = 'DATABASE'
                fields      = @()
                version     = 1
            }
            DEVICE      = [PSCustomObject]@{
                lastUpdated = ''
                dateCreated = ''
                domain      = 'DEVICE'
                fields      = @()
                version     = 1
            }
            STORAGE     = [PSCustomObject]@{
                lastUpdated = ''
                dateCreated = ''
                domain      = 'STORAGE'
                fields      = @()
                version     = 1
            }
        }

        ## Handle Single JSON field files from folders
        $SharedFieldFiles = Get-ChildItem -Path (Join-Path $FieldSpecsFolderPath 'Shared') -Filter '*.json' -Force -ErrorAction 'SilentlyContinue'
        foreach ($FieldFile in $SharedFieldFiles) {
            $FieldSpecs.APPLICATION.fields += (Get-Content -Path $FieldFile.FullName | ConvertFrom-Json)
            $FieldSpecs.DATABASE.fields += (Get-Content -Path $FieldFile.FullName | ConvertFrom-Json)
            $FieldSpecs.DEVICE.fields += (Get-Content -Path $FieldFile.FullName | ConvertFrom-Json)
            $FieldSpecs.STORAGE.fields += (Get-Content -Path $FieldFile.FullName | ConvertFrom-Json)
        }

        $ApplicationFieldFiles = Get-ChildItem -Path (Join-Path $FieldSpecsFolderPath 'Application') -Filter '*.json' -Force -ErrorAction 'SilentlyContinue'
        foreach ($FieldFile in $ApplicationFieldFiles) {
            $FieldSpecs.APPLICATION.fields += (Get-Content -Path $FieldFile.FullName | ConvertFrom-Json)
        }

        $DatabaseFieldFiles = Get-ChildItem -Path (Join-Path $FieldSpecsFolderPath 'Database') -Filter '*.json' -Force -ErrorAction 'SilentlyContinue'
        foreach ($FieldFile in $DatabaseFieldFiles) {
            $FieldSpecs.DATABASE.fields += (Get-Content -Path $FieldFile.FullName | ConvertFrom-Json)
        }

        $DeviceFieldFiles = Get-ChildItem -Path (Join-Path $FieldSpecsFolderPath 'Device') -Filter '*.json' -Force -ErrorAction 'SilentlyContinue'
        foreach ($FieldFile in $DeviceFieldFiles) {
            $FieldSpecs.DEVICE.fields += (Get-Content -Path $FieldFile.FullName | ConvertFrom-Json)
        }

        $StorageFieldFiles = Get-ChildItem -Path (Join-Path $FieldSpecsFolderPath 'Storage') -Filter '*.json' -Force -ErrorAction 'SilentlyContinue'
        foreach ($FieldFile in $StorageFieldFiles) {
            $FieldSpecs.STORAGE.fields += (Get-Content -Path $FieldFile.FullName | ConvertFrom-Json)
        }

    } else {

        ## Check Extension Package <v3 (Single FieldSpecs.json file)
        $FieldSpecsFilePath = Join-Path $ExtensionPath 'FieldSpecs.json'
        if (Test-Path $FieldSpecsFilePath -ErrorAction SilentlyContinue) {

            ## Version 6.0.2.1+ (and TODO version 6.1?, 5x?)
            ## Requires the class names to be lower case
            if ($TMSessionConfig.TMVersion -ge '6.1.0') {
                ## Convert each Asset Class name to lowercase
                $FieldSpecsData = ((Get-Content -Path $FieldSpecsFilePath) -replace 'constraints', 'constraint')
            } else {
                ## No conversion is necessary, assign the provided fields as the updates
                $FieldSpecsData = (((Get-Content -Path $FieldSpecsFilePath) -replace 'constraint', 'constraints') -replace 'constraintss', 'constraints')
            }
        }
        $FieldSpecs = $FieldSpecsData | ConvertFrom-Json
    }

    ## Set the Collected field specs to the Inventory data
    $Inventory.FieldSpecs = $FieldSpecs

    # Create the package object which will be written to a file
    $Package = [PSCustomObject]@{
        name        = $Name.ToLower().Replace(' - ', '-').Replace(' ', '-')
        version     = '1.0.0'
        productName = $Name
        description = $Description ?? ''
        homepage    = ''
        email       = $Email ?? ''
        author      = $Author ?? ''
        published   = $false
        extension   = @{
            packageFormatVersion = 3
            dependencies         = @{
                installedApplications = @(
                    @{
                        name    = 'PowerShell'
                        type    = 'Software'
                        version    = "^$($PSVersionTable.PSVersion.ToString())"
                        source  = 'https://www.github.com/TransitionManager/TMD-Resources/PowerShell'
                    }
                )
                powershellModules     = @(
                    @{
                        name    = '<PLACEHOLDER>'
                        version = '1.0.0'
                        source  = ''
                    }
                )
                extensions            = @(
                    @{
                        name    = '<PLACEHOLDER>'
                        version = '1.0.0'
                    }
                )
            }
        }
        inventory   = $Inventory
    }

    # Create the file
    $Package | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $Path 'package.json') -Force

    # Send the package back if Passthru was specified
    if ($Passthru) {
        $Package
    }
}

function New-TMExtensionInventory {
    <#
    .SYNOPSIS
    Creates a new inventory.json file with all of the configuration assets it contains

    .DESCRIPTION
    This function will collect all of the TM Server/Project configuration artifiacts to an inventory.json file



    .PARAMETER TMSession
    The TM Session to use

    .PARAMETER Path
    Output folder or file path for the inventory file. Include '.json' in the file name to create that file, otherwise the path is used as a Directory.

    .PARAMETER Passthru
    Return the TMConfigInventory File

    .EXAMPLE
    New-TMConfigInventory -TMProfile Default -Path 'C:\Temp\testfile.json'

    .OUTPUTS
    A [PSCustomObject] if -Passthru is specified, otherwise nothing
    #>


    param(
        [Parameter(Mandatory = $false, Position = 0)]
        [String]$TMSession = 'Default',

        [Parameter(Mandatory = $false)]
        [String]$InventoryPath,

        [Parameter(Mandatory = $false)]
        [String]$Server = $global:TMSessions[$TMSession].TMServer,

        [Parameter(Mandatory = $false, Position = 1)]
        [String]$OutPath,
        # (

        <#
            ## Root the folder on the User's Reference Design folder
            $UserPaths.referencedesigns

            ## Add a directory for the Server
            $Server

            ## Add a Folder for the TM Project
            $global:TMSessions[$TMSession].UserContext.Project.Name

            ## Name the file
            'Inventory.json'
            #>


        # Join-Path $UserPaths.referencedesigns $Server $global:TMSessions[$TMSession].UserContext.Project.Name 'Inventory.json'
        # ),

        [Parameter(Mandatory = $false)]$AllowInsecureSSL = $global:TMSessions[$TMSession].AllowInsecureSSL,

        [Parameter(Mandatory = $false)]
        [Switch]$Passthru

    )

    ## Test the optional input file path
    if ($OutPath) {

        ## Test if a Json file was named
        if ($OutPath -like '*.json') {

            ## Parse the path
            $ParentDirectory = Split-Path -Path $OutPath -Parent
            Test-FolderPath -FolderPath $ParentDirectory

        } elseif (-not ([String]::IsNullOrWhiteSpace($OutPath))) {
            Test-FolderPath -FolderPath $OutPath
            $OutPath += (Join-Path $OutPath 'Inventory.json').FullName
        }
    }


    ## Get Session Configuration
    $TMSessionConfig = $global:TMSessions[$TMSession]
    if (-not $TMSessionConfig) {
        Write-Host 'TMSession: [' -NoNewline
        Write-Host $TMSession -ForegroundColor Cyan
        Write-Host '] was not Found. Please use the New-TMSession command.'
        Throw 'TM Session Not Found. Use New-TMSession command before using features.'
    }

    ##
    ## Collect the Inventory
    ##

    ## Read the inventory path if there is one provided
    if (Test-Path $InventoryPath -ErrorAction SilentlyContinue) {
        try {
            $Data = Get-Content $InventoryPath | ConvertFrom-Json -ErrorAction 'SilentlyContinue'
        } catch {
            Write-Host 'The Inventory file could not be converted from json. Performing new Inventory using the TM Server settings supplied' -ForegroundColor Yellow
        }
    }

    ## Allow reading from a server connection, if no Inventory was provided
    if (-Not $Data) {

        ## Create the inventory Package
        $Data = @{
            Inventory     = @{

                ## Add Useful Data
                TMServer        = $TMServer
                TMProject       = $TMProject
                DateCollected   = (Get-Date).ToLongDateString()

                actions         = [System.Collections.ArrayList]::new()
                dataScripts     = [System.Collections.ArrayList]::new()
                recipes         = [System.Collections.ArrayList]::new()
                bundles         = [System.Collections.ArrayList]::new()
                providers       = [System.Collections.ArrayList]::new()
                dependencyTypes = [System.Collections.ArrayList]::new()
                fieldSpecs      = @{}
                Artifacts       = [System.Collections.ArrayList]::new()
                ArtifactLinks   = [System.Collections.ArrayList]::new()
            }

            Relationships = @{
                Providers       = @{}
                Actions         = @{}
                DataScripts     = @{}
                Recipes         = @{}
                Bundles         = @{}
                Fields          = @{}
                DependencyTypes = @{}
            }
        }

        ## Add a 'TM Server'
        [void]$Inventory.Artifacts.Add([PSCustomObject]@{
                Id   = "tmserver-$TMServer"
                Type = 'TMServer'
                Name = $TMServer
            }
        )
        ## Add a 'TM Project'
        [void]$Inventory.Artifacts.Add([PSCustomObject]@{
                Id       = "tmproject-$TMProject"
                Type     = 'TMProject'
                Name     = $TMProject

                ParentId = $TMServer
            }
        )

        ## Collect FieldSpecs
        $Inventory.fieldSpecs = Get-TMFieldSpecs -TMSession $TMSession

        ## Add a 'FieldSpecs' object to the Artifact inventory (for proper diagram structuring)
        [void]$Inventory.Artifacts.Add([PSCustomObject]@{
                Id       = 'tmfieldspecs'
                Type     = 'FieldSpecs'
                Name     = 'FieldSpecs'

                ParentId = "tmproject-$TMProject"
            }
        )

        ## Process each Domain Class
        foreach ($DomainClass in $Inventory.fieldSpecs.PSobject.Properties.Name) {
            ## Add an Artifact to the inventory
            [void]$Inventory.Artifacts.Add([PSCustomObject]@{
                    Id       = "tmfieldspecdomain-$DomainClass"
                    Type     = 'FieldSpecDomain'
                    Name     = $DomainClass

                    ParentId = 'tmfieldspecs'
                }
            )
            ## Handle Each Domain Class field
            $ClassFields = $Inventory.fieldSpecs.$DomainClass.fields
            foreach ($Field in $ClassFields) {

                ## Add a property for this Field in the Relationships data
                $Data.Relationships.Fields.("$($DomainClass)-$($Field.label)") = @{
                    Actions         = [System.Collections.ArrayList]@()
                    DataScripts     = [System.Collections.ArrayList]@()
                    Recipes         = [System.Collections.ArrayList]@()
                    RecipeGroups    = [System.Collections.ArrayList]@()
                    RecipeTaskSpecs = [System.Collections.ArrayList]@()
                }

                ## Add a AssetClass for each domain to the inventory
                [void]$Inventory.Artifacts.Add([PSCustomObject]@{
                        Id          = "tmassetfield-$($DomainClass)-$($Field.label)"
                        Type        = 'AssetField'
                        Name        = $Field.label

                        Constraints = $Field.constraints
                        DomainClass = $DomainClass
                        ParentId    = "tmfieldspecdomain-$DomainClass"
                    }
                )
            }
        }

        ## Collect Actions
        $Inventory.actions = Get-TMAction -Passthru -TMSession $TMSession
        foreach ($Item in $Inventory.actions) {

            ## Add a property for this Field in the Relationships data
            $Data.Relationships.Actions.($Item.name) = @{
                Fields          = [System.Collections.ArrayList]@()
                Recipes         = [System.Collections.ArrayList]@()
                RecipeTaskSpecs = [System.Collections.ArrayList]@()
            }

            ## Add an Artifact to the inventory
            [void]$Inventory.Artifacts.Add([PSCustomObject]@{
                    Id       = "tmaction-$($Item.id)"
                    Type     = 'Action'
                    Name     = $Item.name

                    ParentId = "tmprovider-$($item.provider.id)"
                }
            )
        }

        ## Collect Data Scripts
        $Inventory.dataScripts = Get-TMDatascript -Passthru -TMSession $TMSession
        foreach ($Item in $Inventory.dataScripts) {

            ## Add a property for this Field in the Relationships data
            $Data.Relationships.DataScripts.($Item.name) = @{
                Fields = [System.Collections.ArrayList]@()
            }

            ## Add an Artifact to the inventory
            [void]$Inventory.Artifacts.Add([PSCustomObject]@{
                    Id       = "tmdatascript-$($Item.id)"
                    Type     = 'DataScript'
                    Name     = $Item.name

                    ParentId = "tmprovider-$($item.provider.id)"
                }
            )
        }

        ## Collect Recipes
        $Inventory.recipes = Get-TMRecipe -AsJson -Passthru -TMSession $TMSession
        foreach ($Item in $Inventory.recipes) {

            ## Add a property for this Field in the Relationships data
            $Data.Relationships.Recipes.($Item.name) = @{
                Actions         = [System.Collections.ArrayList]@()
                RecipeGroups    = [System.Collections.ArrayList]@()
                RecipeTaskSpecs = [System.Collections.ArrayList]@()
                Fields          = [System.Collections.ArrayList]@()
            }

            ## Add an Artifact to the inventory
            [void]$Inventory.Artifacts.Add([PSCustomObject]@{
                    Id       = "tmrecipe-$($Item.id)"
                    Type     = 'Recipe'
                    Name     = $Item.name

                    ParentId = "tmprovider-$($item.provider.id)"
                }
            )
        }

        ## Collect Bundles
        $Inventory.bundles = Get-TMBundle -TMSession $TMSession
        foreach ($Item in $Inventory.bundles) {

            ## Add an Artifact to the inventory
            [void]$Inventory.Artifacts.Add([PSCustomObject]@{
                    Id       = "tmbundle-$($Item.id)"
                    Type     = 'Bundle'
                    Name     = $Item.name

                    ParentId = "tmproject-$TMProject"
                }
            )
        }

        ## Collect Providers
        $Inventory.providers = Get-TMProvider -TMSession $TMSession
        foreach ($Item in $Inventory.providers) {

            ## Add an Artifact to the inventory
            [void]$Inventory.Artifacts.Add([PSCustomObject]@{
                    Id       = "tmprovider-$($Item.id)"
                    Type     = 'Provider'
                    Name     = $Item.name

                    ParentId = "tmproject-$TMProject"
                }
            )

            ## Add a Provider-Actions Artifact
            [void]$Inventory.Artifacts.Add([PSCustomObject]@{
                    Id       = "tmprovider-actions-$($Item.id)"
                    Type     = 'ProviderActions'
                    Name     = $Item.name + ' Actions'

                    ParentId = "tmprovider-$($Item.id)"
                }
            )

            ## Add a Provider-Actions Artifact
            [void]$Inventory.Artifacts.Add([PSCustomObject]@{
                    Id       = "tmprovider-datascripts-$($Item.id)"
                    Type     = 'ProviderActions'
                    Name     = $Item.name + ' Data Scripts'

                    ParentId = "tmprovider-$($Item.id)"
                }
            )
        }

        ## Collect Dependency Types
        $Inventory.dependencyTypes = Get-TMDependencyType -TMSession $TMSession
        foreach ($Item in $Inventory.dependencyTypes) {

            ## Add an Artifact to the inventory
            [void]$Inventory.Artifacts.Add([PSCustomObject]@{
                    Id       = "tmdependencytype-$($Item.label)"
                    Type     = 'DependencyType'
                    Name     = $Item.label

                    ParentId = "tmserver-$TMServer"
                }
            )
        }

        ##
        ## Save the Inventory data for convenience
        ##
        if (-Not ([String]::IsNullOrWhiteSpace($InventoryPath))) {
            $Data | ConvertTo-Json -Depth 100 -Compress | Set-Content -Path $InventoryPath -Force
        }
    }

    ##
    ## Calculate the Artifact Relationships
    ##

    ## ACTION(parameter) > Asset Field
    foreach ($Action in $Inventory.actions) {

        if ($Action.MethodParams) {

            ## Iterater on each Method Parameter
            foreach ($Param in $Action.methodParams) {

                ## only map params that use a field label
                if ($Param.context -ne 'USER_DEF' -and $Param.fieldLabel) {

                    ## Record the connection in the hashmap
                    [void]($Data.Relationships.Actions.($Action.Name).Fields.Add("$($Param.context)-$($Param.fieldLabel)"))

                    ## Add the Artifact Link
                    [void]$Inventory.ArtifactLinks.Add([PSCustomObject]@{
                            Id   = "tmaction-$($Action.Id)-tmassetfield-$($Param.context)-$($Param.fieldLabel)"
                            From = "tmaction-$($Action.Id)"
                            To   = "tmassetfield-$($Param.context)-$($Param.fieldLabel)"
                            type = 'TM Server hosts Project'
                        }
                    )
                }
            }
        }
    }

    ## Recipe > Actions, Groups, TaskSpecs
    foreach ($Recipe in $Inventory.recipes) {

        ## Create a Groups object
        [void]$Inventory.Artifacts.Add([PSCustomObject]@{
                Id       = "tmrecipe-$($Recipe.id)-groups"
                Type     = 'RecipeGroup'
                Name     = $Recipe.name + ' Groups'

                ParentId = "tmrecipe-$($Recipe.id)"
            }
        )

        ## Create a TaskSpecs object
        [void]$Inventory.Artifacts.Add([PSCustomObject]@{
                Id       = "tmrecipe-$($Recipe.id)-taskspecs"
                Type     = 'RecipeGroup'
                Name     = $Recipe.name + ' Groups'

                ParentId = "tmrecipe-$($Recipe.id)"
            }
        )

        ## Check each Group in the recipe
        foreach ($Group in $Recipe.sourceCodeJson.groups) {

            ## Record the connection in the hashmap
            [void]($Data.Relationships.Recipes.($Recipe.Name).RecipeGroups.Add($Group.name))

            ## Record the Group
            [void]$Inventory.Artifacts.Add([PSCustomObject]@{
                    Id       = "tmrecipegroup-$($Recipe.id)-$($group.name)"
                    Type     = 'RecipeGroup'
                    Name     = $group.name

                    ParentId = "tmrecipe-$($Recipe.id)-groups"
                }
            )

            ## TODO - Add Group Asset Field resolution so the groups can be bound to the Asset-Fields
        }

        ## Check each Task Spec in the recipe
        foreach ($TaskSpec in $Recipe.sourceCodeJson.tasks) {

            ## Record the connection in the hashmap
            [void]($Data.Relationships.Recipes.($Recipe.Name).RecipeTaskSpecs.Add($TaskSpec.title))

            ## Record the Task Spec
            [void]$Inventory.Artifacts.Add([PSCustomObject]@{
                    Id       = "tmtaskspec-$($Recipe.id)-$($TaskSpec.id)"
                    Type     = 'RecipeTaskSpec'
                    Name     = $TaskSpec.title

                    ParentId = "tmrecipe-$($Recipe.id)-taskspecs"
                }
            )

            ## Record Tasks that have a method assigned
            if ($TaskSpec.invoke.method) {

                ## Record the connection in the hashmap
                [void]($Data.Relationships.Recipes.($Recipe.Name).Actions.Add($TaskSpec.invoke.method))

                [void]$Inventory.ArtifactLinks.Add([PSCustomObject]@{
                        Id   = "tmaction-$($Action.Id)-tmtaskspec-$($Recipe.id)-$($TaskSpec.id)"
                        From = "tmaction-$($Action.Id)"
                        To   = "tmtaskspec-$($Recipe.id)-$($TaskSpec.id)"
                        type = 'TM Server hosts Project'
                    }
                )
            }

            ## Record Tasks that have a method assigned
            if ($TaskSpec.filter.group) {

                foreach ($Group in $TaskSpec.filter.group) {

                    ## Record the connection in the hashmap
                    [void]($Data.Relationships.Recipes.($Recipe.Name).RecipeGroups.Add($Group.name))

                    [void]$Inventory.ArtifactLinks.Add([PSCustomObject]@{
                            Id   = "tmaction-$($Action.Id)-tmtaskspec-$($Recipe.id)-$($TaskSpec.id)"
                            From = "tmaction-$($Action.Id)"
                            To   = "tmrecipegroup-$($Recipe.id)-$($group.name)"
                            type = 'TM Server hosts Project'
                        }
                    )
                }
            }
        }
    }


    ## Datascripts to
    ## > Asset Field

    # Send the package back if Passthru was specified
    if ($Passthru) {
        $Data
    }

    # ## Create a flat inventory of Artifacts


    # # Send the package back if Passthru was specified
    # if ($Passthru) {
    # $Inventory
    # }
}
function Read-TMExtensionInventory {
    <#
    .SYNOPSIS
    Creates a new inventory.json file with all of the configuration assets it contains, as read from an Extension directory

    .DESCRIPTION
    This function will read all of the TM Entities within an Extension directory

    .PARAMETER Path
    Path of the TM Extension to read

    .PARAMETER OutPath
    Filename to create containing the inventory

    .PARAMETER Passthru
    Return the TMConfigInventory File

    .EXAMPLE
    Read-TMConfigInventory -Path 'C:\Users\tbaker\TMD_Files\Reference Designs\EXTENSIONS\VMware\vCenter' -OutPath 'C:\Temp\testfile.json' -Passthru

    .OUTPUTS
    A [PSCustomObject] if -Passthru is specified, otherwise nothing
    #>


    param(

        [Parameter(Mandatory = $false, Position = 0)]
        [String]$TMSession = 'Default',

        [Parameter(Mandatory = $true, Position = 1)]
        [String]$Path,

        [Parameter(Mandatory = $false, Position = 2)]
        [String]$OutPath,

        [Parameter(Mandatory = $false)]
        [Switch]$Passthru

    )

    ## Create the inventory Package
    $Inventory = @{
        Actions         = [System.Collections.ArrayList]::new()
        DataScripts     = [System.Collections.ArrayList]::new()
        Recipes         = [System.Collections.ArrayList]::new()
        Bundles         = [System.Collections.ArrayList]::new()
        Providers       = [System.Collections.ArrayList]::new()
        DependencyTypes = [System.Collections.ArrayList]::new()
        FieldSpecs      = @{}
        Artifacts       = [System.Collections.ArrayList]::new()
        ArtifactLinks   = [System.Collections.ArrayList]::new()

        Relationships   = @{
            Providers       = @{}
            Actions         = @{}
            DataScripts     = @{}
            Recipes         = @{}
            Bundles         = @{}
            Fields          = @{}
            DependencyTypes = @{}
        }
    }

    ## Lookup the local package file to get the name and
    $PackageFilePath = "$Path\package.json"
    if (Test-Path $PackageFilePath -ErrorAction SilentlyContinue) {
        $PackageData = Get-Content -Path $PackageFilePath | ConvertFrom-Json
        $Inventory.Extension = @{
            Name        = $PackageData.name
            ProductName = $PackageData.productName
        }
    }

    ## Update the path to the internal _REFERENCE DESIGN folder
    if ($Path -notlike '*_REFERENCE DESIGN') {
        $Path = "$Path\_REFERENCE DESIGN"
    }

    ##
    ## Collect the Inventory
    ##
    #

    ## Collect JSON File data
    $ErrorActionPreference = 'SilentlyContinue'
    $Inventory.AppTypes = Get-Content (Join-Path $Path 'AppTypes.json') | ConvertFrom-Json -ErrorAction 'SilentlyContinue'
    $Inventory.Bundles = Get-Content (Join-Path $Path 'Bundles.json') | ConvertFrom-Json -ErrorAction 'SilentlyContinue'
    $Inventory.DeviceTypes = Get-Content (Join-Path $Path 'DeviceTypes.json') | ConvertFrom-Json -ErrorAction 'SilentlyContinue'
    $Inventory.Providers = Get-Content (Join-Path $Path 'Providers.json') | ConvertFrom-Json -ErrorAction 'SilentlyContinue'
    $Inventory.Tags = Get-Content (Join-Path $Path 'Tags.json') | ConvertFrom-Json -ErrorAction 'SilentlyContinue'
    $Inventory.TaskCategories = Get-Content (Join-Path $Path 'TaskCategories.json') | ConvertFrom-Json -ErrorAction 'SilentlyContinue'
    $Inventory.Teams = Get-Content (Join-Path $Path 'Teams.json') | ConvertFrom-Json -ErrorAction 'SilentlyContinue'
    $Inventory.FieldSpecs = Get-Content (Join-Path $Path 'FieldSpecs.json') | ConvertFrom-Json -ErrorAction 'SilentlyContinue'
    $ErrorActionPreference = 'Continue'

    ## Create Relationship properties for each of the fields
    # foreach ($DomainClass in $Inventory.FieldSpecs.PSobject.Properties.Name) {

    # ## Handle Each Domain Class field
    # $ClassFields = $Inventory.fieldSpecs.$DomainClass.fields
    # foreach ($Field in $ClassFields) {

    # ## Add a property for this Field in the Relationships data
    # $Inventory.Relationships.Fields.("$($DomainClass)-$($Field.label)") = @{
    # Actions = [System.Collections.ArrayList]@()
    # DataScripts = [System.Collections.ArrayList]@()
    # Recipes = [System.Collections.ArrayList]@()
    # RecipeGroups = [System.Collections.ArrayList]@()
    # RecipeTaskSpecs = [System.Collections.ArrayList]@()
    # }
    # }
    # }

    ## Create Relationship properties for each of the Providers
    # foreach ($Provider in $Inventory.Providers) {
    # $Inventory.Relationships.Providers.($Provider.Name) = @{
    # Actions = [System.Collections.ArrayList]@()
    # DataScripts = [System.Collections.ArrayList]@()
    # }
    # }

    ## Collect Actions
    $ActionFiles = Get-ChildItem -Path "$Path\Actions" -Recurse -Filter '*.ps1' -ErrorAction SilentlyContinue
    foreach ($Item in $ActionFiles) {

        $Action = Read-TMActionScriptFile -Path $Item.FullName -TMSession $TMSession

        ## Add an Artifact to the inventory
        [void]$Inventory.Actions.Add($Action)

        ## Add a property for this Field in the Relationships data
        # $Inventory.Relationships.Actions.($Action.name) = @{
        # Provider = $Action.provider.name
        # Fields = [System.Collections.ArrayList]@()
        # Recipes = [System.Collections.ArrayList]@()
        # RecipeTaskSpecs = [System.Collections.ArrayList]@()
        # }

        # ## Add this Action to the Provider's list as well
        # [void]($Inventory.Relationships.Providers.($Action.provider.name).Actions.Add($Action.Name))
    }

    ## Collect Data Scripts
    $DataScriptFiles = Get-ChildItem -Path "$Path\DataScripts" -Recurse -Filter '*.groovy' -ErrorAction SilentlyContinue
    foreach ($Item in $DataScriptFiles) {

        $DataScript = Read-TMDatascriptFile -Path $Item.FullName

        ## Add an Artifact to the inventory
        [void]$Inventory.DataScripts.Add($DataScript)

        ## Add a property for this Field in the Relationships data
        # $Inventory.Relationships.DataScripts.($DataScript.name) = @{
        # Provider = $ActioDatascript.provider.name
        # Fields = [System.Collections.ArrayList]@()
        # }

        # ## Add this Datascript to the Provider's list as well
        # [void]($Inventory.Relationships.Providers.($DataScript.provider.name).Datascripts.Add($DataScript.Name))
    }

    ## Create a flat inventory of Artifacts


    # Send the package back if Passthru was specified
    if (-not ([String]::IsNullOrWhiteSpace($OutPath))) {
        $Inventory | ConvertTo-Json -Depth 100 | Set-Content -Path $OutPath -Force
    }

    # Send the package back if Passthru was specified
    if ($Passthru) {
        $Inventory
    }
}