lib/Actions.ps1



# Actions
Function New-TMAction {
    param(
        [Parameter(Mandatory = $false)][String]$TMSession = 'Default',
        [Parameter(Mandatory = $true)][PSObject]$Action,
        [Parameter(Mandatory = $false)][String]$Server = $global:TMSessions[$TMSession].TMServer,
        [Parameter(Mandatory = $false)]$AllowInsecureSSL = $global:TMSessions[$TMSession].AllowInsecureSSL,
        [Parameter(Mandatory = $false)][PSObject]$Project,
        [Parameter(Mandatory = $false)][Switch]$Update,
        [Parameter(Mandatory = $false)][Switch]$Passthru
    )

    ## Use the TM Session provided
    if ($global:TMSessions[$TMSession].TMVersion -like '4.6*') {
        New-TMAction46 @PSBoundParameters

    } else {
        New-TMAction474 @PSBoundParameters
    }
}
Function New-TMAction46 {
    param(
        [Parameter(Mandatory = $false)][String]$TMSession = 'Default',
        [Parameter(Mandatory = $true)][PSObject]$Action,
        [Parameter(Mandatory = $false)][String]$Server = $global:TMSessions[$TMSession].TMServer,
        [Parameter(Mandatory = $false)]$AllowInsecureSSL = $global:TMSessions[$TMSession].AllowInsecureSSL,
        [Parameter(Mandatory = $false)][PSObject]$Project,
        [Parameter(Mandatory = $false)][Switch]$Passthru

    )

    ## 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.'
    }

    #Honor SSL Settings
    if ($TMSessionConfig.AllowInsecureSSL) {
        $TMCertSettings = @{SkipCertificateCheck = $true }
    } else {
        $TMCertSettings = @{SkipCertificateCheck = $false }
    }

    ## Check for existing Action
    $ActionCheck = Get-TMAction -Name $Action.name -TMSession $TMSession
    if ($ActionCheck) {
        if ($Passthru) {
            return $ActionCheck
        } else {
            return
        }
    } else {
        ## No Credential exists. Create it
        $instance = $Server.Replace('/tdstm', '')
        $instance = $instance.Replace('https://', '')
        $instance = $instance.Replace('http://', '')

        $uri = 'https://'
        $uri += $instance
        $uri += '/tdstm/ws/apiAction'

        ## Lookup Cross References
        if (-not $Project) {
            $Project = $Projects | Where-Object { $_.name -eq $Action.project.name }
        }
        $ProjectID = $Project.id

        $ProviderID = (Get-TMProvider -TMSession $TMSession | Where-Object { $_.name -eq $Action.provider.name }).id
        $CredentialID = (Get-TMCredential -TMSession $TMSession | Where-Object { $_.name -eq $Action.credential.name }).id

        ## Fix up the object
        $Action.PSObject.properties.Remove('id')
        $Action.actionType = $Action.actionType.id
        $Action.project.id = $ProjectID
        $Action.provider.id = $ProviderID
        $Action.credential.id = $CredentialID
        $Action.version = 1
        $PostBodyJSON = $Action | ConvertTo-Json -Depth 100

        Set-TMHeaderAccept 'JSON' -TMSession $TMSession
        Set-TMHeaderContentType 'JSON' -TMSession $TMSession
        try {
            $response = Invoke-WebRequest -Method Post -Uri $uri -WebSession $TMSessionConfig.TMWebSession -Body $PostBodyJSON @TMCertSettings
            if ($response.StatusCode -eq 200) {
                $responseContent = $response.Content | ConvertFrom-Json
                if ($responseContent.status -eq 'success') {
                    if ($Passthru) {
                        return $responseContent.data
                    } else {
                        return
                    }
                }
            }
        } catch {
            Write-Host 'Unable to create Action.'
            return $_
        }
    }
}
Function New-TMAction474 {
    param(
        [Parameter(Mandatory = $false)][String]$TMSession = 'Default',
        [Parameter(Mandatory = $true)][PSObject]$Action,
        [Parameter(Mandatory = $false)][String]$Server = $global:TMSessions[$TMSession].TMServer,
        [Parameter(Mandatory = $false)]$AllowInsecureSSL = $global:TMSessions[$TMSession].AllowInsecureSSL,
        [Parameter(Mandatory = $false)][PSObject]$Project,
        [Parameter(Mandatory = $false)][Switch]$Update,
        [Parameter(Mandatory = $false)][Switch]$Passthru
    )
    ## 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.'
    }

    #Honor SSL Settings
    if ($TMSessionConfig.AllowInsecureSSL) {
        $TMCertSettings = @{SkipCertificateCheck = $true }
    } else {
        $TMCertSettings = @{SkipCertificateCheck = $false }
    }

    ## Check for existing Action
    $ActionCheck = Get-TMAction -Name $Action.name -TMSession $TMSession
    if ($ActionCheck -and -not $Update) {
        if ($PassThru) {
            return $ActionCheck
        } else {
            return
        }
    } else {


        ## No Action exists. Create it
        $instance = $Server.Replace('/tdstm', '')
        $instance = $instance.Replace('https://', '')
        $instance = $instance.Replace('http://', '')

        $uri = 'https://'
        $uri += $instance
        $uri += '/tdstm/ws/apiAction'

        ## Cleans the Action Name of characters we don't want
        $Action.name = $Action.name -replace '\\', '' -replace '\/', '' -replace '\:', '' -replace '>', '' -replace '<', '' -replace '\(', '' -replace '\)', '' -replace '\*', ''
        # $Action.name = $SafeActionName

        ## If The Existing action should be updated
        if ($ActionCheck -and $Update) {

            ## When the Action is an update, use important details from the current object.
            $Action.id = $ActionCheck.id
            $Action.dateCreated = $ActionCheck.dateCreated
            $Action.lastUpdated = $ActionCheck.lastUpdated
            # $Action.debugEnabled = $ActionCheck.debugEnabled
            $Action.version = $ActionCheck.version
            $action.project = $ActionCheck.project

            ## Set the HTTP call details
            $uri += '/' + $Action.id
            $HttpMethod = 'Put'
        } else {

            ## Process as a new object.
            $HttpMethod = 'Post'
            $Action.PSObject.Properties.Remove('id')
        }

        ## Lookup Provider and Credential IDs, Field Specs
        if ([String]::IsNullOrWhiteSpace($Action.provider.name)) {
            Write-Error -Message "Provider name is blank for Action: $($Action.name)"
            return
        }
        $ProviderID = (Get-TMProvider -Name $Action.provider.name -TMSession $TMSession).id
        if (!$ProviderID) {
            # Create the provider if it doesn't exist
            $NowFormatted = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ' -AsUTC).ToString()
            $Provider = [PSCustomObject]@{
                id          = $null
                name        = $Action.provider.name
                description = ''
                comment     = ''
                dateCreated = $NowFormatted
                lastUpdated = $NowFormatted
            }
            $ProviderID = (New-TMProvider -Provider $Provider -PassThru -TMSession $TMSession).id
        }
        $Action.provider.id = $ProviderID

        ## If the Credential name is provided, look up the proper credential and add the ID
        if ($Action.credential) {
            if ($Action.credential.name) {
                $CredentialID = (Get-TMCredential -Name $Action.credential.name -TMSession $TMSession ).id
                if ($CredentialID) { $Action.credential.id = $CredentialID }
                $Action.remoteCredentialMethod = 'SUPPLIED'
            } else {
                $Action.credential = $null
                $Action.remoteCredentialMethod = 'USER_PRIV'
            }
        }

        $FieldSettings = Get-TMFieldSpecs -TMSession $TMSession

        ## Fix the Parameters to the correct custom field name
        if ($Action.methodParams) {

            ## Unwrap
            if ($Action.methodParams.GetType().Name -eq 'String') {
                $Parameters = $Action.methodParams | ConvertFrom-Json
            } else {
                $Parameters = $Action.methodParams
            }

            for ($j = 0; $j -lt $Parameters.Count; $j++) {
                if ($Parameters[$j].context -in @('DEVICE', 'APPLICATION', 'STORAGE', 'DATABASE')) {

                    ## Check if a fieldLabel param exists in the Parameters[$j].
                    ## This is added by the New-TMIntegrationPlugin command so the correct
                    ## Custom fields can be assigned when the new Action is installed.
                    if ($Parameters[$j].PSobject.Properties.name -match 'fieldLabel') {

                        ## Check if this is a CustomN field
                        if ($Parameters[$j].fieldName -eq 'customN') {

                            ## Update the Project's assigned Field by associating the label to the current field list.
                            $Parameters[$j].fieldName = ($FieldSettings.($Parameters[$j].context).fields | Where-Object { $_.label -eq $Parameters[$j].fieldLabel }).field
                        }
                    }
                }
                ## Remove the 'fieldLabel property as it's invalid column definition
                $Parameters[$j].PSObject.Properties.Remove('fieldLabel')
            }
            if ($Parameters) {
                $Action.methodParams = (ConvertTo-Array -InputObject $Parameters | ConvertTo-Json -Depth 100 -Compress ).toString()
            } else {
                $Action.PSObject.Properties.Remove('methodParams')
            }
        }

        ## Adjust the actionType for TM6.1
        if ($TMSessionConfig.TMVersion -ge '6.1.0') {
            $Action.actionType = $Action.actionType.id ?? $Action.actionType
        }

        $PostBodyJSON = $Action | ConvertTo-Json

        Set-TMHeaderAccept 'JSON' -TMSession $TMSession
        Set-TMHeaderContentType 'JSON' -TMSession $TMSession
        try {
            $response = Invoke-WebRequest -Method $HttpMethod -Uri $uri -WebSession $TMSessionConfig.TMWebSession -Body $PostBodyJSON @TMCertSettings
            if ($response.StatusCode -eq 200) {
                $responseContent = $response.Content | ConvertFrom-Json
                if ($responseContent.status -ne 'success') {
                    throw ($responseContent.errors -join ', ')
                } else {
                    if ($PassThru) {
                        return $responseContent.data
                    }
                }
            } elseif ($response.StatusCode -eq 204) {
                return
            }
        } catch {
            Write-Host 'Unable to create Action.'
            return $_
        }
    }

}
Function Get-TMAction {
    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        [Parameter(Mandatory = $false)]
        [String]$TMSession = 'Default',

        [Parameter(Mandatory = $false, ParameterSetName = 'ByName')]
        [String[]]$Name,

        [Parameter(Mandatory = $false, ParameterSetName = 'ById')]
        [String[]]$Id,

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

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

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

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

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

        [Parameter(Mandatory = $false)]
        [bool]$Api = $true,

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

    ## 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.'
    }

    #Honor SSL Settings
    $TMCertSettings = $TMSessionConfig.AllowInsecureSSL ? @{SkipCertificateCheck = $true } : @{SkipCertificateCheck = $false }

    $instance = $Server.Replace('/tdstm', '').Replace('https://', '').Replace('http://', '')
    if ($Api) {

        # Format the uri
        $uri = "https://$instance/tdstm/api/apiAction?project=$($TMSessionConfig.UserContext.project.id)"

        try {
            $response = Invoke-RestMethod -Method Get -Uri $uri -WebSession $TMSessionConfig.TMRestSession @TMCertSettings
            $Result = $response | Where-Object { $_.actionType.id -EQ 'POWER_SHELL' }
        } catch {
            throw $_
        }
    } else {

        # Format the uri
        $uri = "https://$instance/tdstm/ws/apiAction"

        try {
            $response = Invoke-WebRequest -Method Get -Uri $uri -WebSession $TMSessionConfig.TMWebSession @TMCertSettings
        } catch {
            throw $_
        }

        if ($response.StatusCode -in @(200, 204)) {
            $Result = ($response.Content | ConvertFrom-Json).data
            $Result = $Result | Where-Object actionType -EQ 'POWER_SHELL'
        } else {
            throw 'Unable to collect Actions.'
        }
    }

    ## Return the details -- Filter based on passed parameters
    if ($ProviderName) {
        $Result = $Result | Where-Object { $_.provider.name -in $ProviderName }
    } elseif ($Name) {
        $Result = $Result | Where-Object { $_.name -in $Name }
    } elseif ($Id) {
        $Result = $Result | Where-Object { $_.id -in $Id }
    }

    ## 6.1 and forward, the endpoints do not supply the source code with the object
    if ($TMSessionConfig.TMVersion -ge '6.0.0') {

        if ($Api) {

            ## Update each of the objects that were returned with their full version
            $Result = $Result | ForEach-Object {
                # Format the uri
                $uri = "https://$instance/tdstm/api/apiAction/$($_.id)?project=$($TMSessionConfig.UserContext.project.id)"
                Invoke-RestMethod -Method Get -Uri $uri -WebSession $TMSessionConfig.TMRestSession @TMCertSettings
            }
        } else {
            ## Update each of the objects that were returned with their full version
            $Result = $Result | ForEach-Object {
                # Format the uri
                $uri = "https://$instance/tdstm/ws/apiAction/$($_.id)?project=$($TMSessionConfig.UserContext.project.id)"
                Invoke-WebRequest -Method Get -Uri $uri -WebSession $TMSessionConfig.TMWebSession @TMCertSettings

                try {
                    $response = Invoke-WebRequest -Method Get -Uri $uri -WebSession $TMSessionConfig.TMWebSession @TMCertSettings
                } catch {
                    throw $_
                }

                if ($response.StatusCode -in @(200, 204)) {
                    ($response.Content | ConvertFrom-Json).data
                } else {
                    throw 'Unable to collect Actions.'
                }
            }
        }
    }

    # Get Field / Label maps to translate the parameters
    $FieldToLabelMap = Get-TMFieldToLabelMap -Server $Server -TMSession $TMSession -AllowInsecureSSL $AllowInsecureSSL

    for ($i = 0; $i -lt $Result.Count; $i++) {

        ## Fix the Parameters to the correct custom field name
        if ($Result[$i].methodParams) {
            if ($Result[$i].methodParams.getType().Name -eq 'String') {
                $Parameters = $Result[$i].methodParams | ConvertFrom-Json
            }
            $Parameters = @(($Parameters | Sort-Object -Property 'paramName'))
            for ($j = 0; $j -lt $Parameters.Count - 1; $j++) {
                if ($Parameters[$j].context.toUpper() -in @('DEVICE', 'APPLICATION', 'STORAGE', 'DATABASE')) {

                    ## The custom column field identifer is lost when the IDs get reset. Add a fieldLabel node to put the custom field label. This will get replaced/updated by the Import
                    $FieldLabel = $FieldToLabelMap."$($Parameters[$j].context.toUpper())"."$($Parameters[$j].fieldName)" ?? ''
                    $Parameters[$j] | Add-Member -NotePropertyName 'fieldLabel' -NotePropertyValue $FieldLabel -Force

                    ## Clear the Custom field number associated to the Parameter
                    if ($ResetIDs -and ($Parameters[$j].fieldName -like 'custom*')) {
                        $Parameters[$j].fieldName = 'customN'
                    }
                }
            }
            $Result[$i].methodParams = @(($Parameters))
        }

        ## Apply Package Format version 2 details
        Add-Member -InputObject $Result[$i] -NotePropertyName 'actionType' -NotePropertyValue ($Result[$i].actionType.id ?? $Result[$i].actionType) -Force
    }

    if ($ResetIDs) {
        ## Clear pertinent data in each Action
        for ($i = 0; $i -lt $Result.Count; $i++) {
            $Result[$i].id = $null
            $Result[$i].project.id = $null
            if ($Result[$i].credential) { $Result[$i].credential.id = $null }
            $Result[$i].provider.id = $null
        }
    }

    ## Save the Code Files to a folder
    if ($SaveCodePath) {

        ## Save Each of the Script Source Data
        foreach ($Item in $Result) {

            ## Convert the 'Type' property to a string/enum
            $Item.actionType = $Item.actionType.id ?? $Item.actionType

            ## Get a FileName safe version of the Provider Name
            $SafeProviderName = $Item.provider.name -replace '\:', '-' -replace '\/', '-' -replace '\|', '-' -replace '\\', '-'
            $SafeActionName = $Item.name -replace '\:', '-' -replace '\/', '-' -replace '\|', '-' -replace '\\', '-'

            ## Create the Provider Action Folder path
            $ProviderPath = Join-Path $SaveCodePath $SafeProviderName
            Test-FolderPath -FolderPath $ProviderPath

            ## Create a File ame for the Action
            $ProviderScriptConfigPath = Join-Path $ProviderPath ($SafeActionName + '.json')
            $ProviderScriptPath = Join-Path $ProviderPath ($SafeActionName + '.ps1')

            ## Split the script out of the Item, and save both files.
            $ScriptBlock = $Item.script
            Set-Content -Path $ProviderScriptPath -Force -Value $ScriptBlock

            ## Remove the script from the JSON file
            $Item.PSObject.Properties.Remove('script')

            ## Convert the Parameters to a TMActionParameters Class Object
            if ($Item.methodParams) {
                $Parameters = $Item.methodParams | ForEach-Object { [TMActionParameters]::new($_) }
                $Item.methodParams = $Parameters
            }
            Set-Content -Path $ProviderScriptConfigPath -Force -Value ($Item | ConvertTo-Json -Depth 10)
        }
    }

    if ($Passthru -or !$SaveCodePath) {
        ## Return a single value, or an Array
        if ($Result.Count -eq 1) {
            return $Result[0]
        } else {
            return $Result
        }
    }
}

Function Read-TMActionScriptFile {
    param(
        [Parameter(Mandatory = $true)]$Path
    )

    ## First order of business is to determine if this Script file has a counterpart Json file
    $ActionConfigJsonPath = $Path -Replace '.ps1', '.json'
    $ActionConfigFile = Get-Item -Path $ActionConfigJsonPath -ErrorAction 'SilentlyContinue'

    ## Check if there is a config JSON file, if so, get the Action from the 2 files
    if ($ActionConfigFile) {

        ## Get the Action Object that doesn't have the source code
        $TMAction = Get-Content -Path $ActionConfigFile | ConvertFrom-Json

        $FieldToLabelMap = Get-TMLabelToFieldMap

        ## If this Action has parameters, handle sorting and updating them
        if ($TMAction.methodParams) {

            ## Convert the methodParams to a String, as it should be
            switch ($TMAction.methodParams.GetType().Name) {
                ## Array of objects, each is a PSCustomObject of a TMActionParameter type
                'Object[]' {

                    ## Remove Offline properties that do not belong on the object.
                    $TMAction.methodParams = $TMAction.methodParams | Sort-Object -Property 'order' | ForEach-Object {

                        ## Add the correct TM Field Label
                        if ($_.fieldName -eq 'customN') {
                            $_.fieldName = $FieldToLabelMap.($_.context.toUpper()).($_.fieldLabel)
                        }

                        $_.PSObject.Properties.Remove('order')
                        $_.PSObject.Properties.Remove('fieldLabel')
                        $_

                    } | ConvertTo-Json -Depth 5 -Compress
                    break
                }

                ## There might only be one Param (and was written without being forced to a list)
                'PSCustomObject' {
                    ## Add the correct TM Field Label
                    if ($TMAction.methodParams.fieldName -eq 'customN') {
                        $TMAction.methodParams.fieldName = $FieldToLabelMap.($TMAction.methodParams.context.toUpper()).($TMAction.methodParams.fieldLabel)
                    }
                    $TMAction.methodParams.PSObject.Properties.Remove('order')
                    $TMAction.methodParams.PSObject.Properties.Remove('fieldLabel')
                    $TMAction.methodParams = @(($TMAction.methodParams)) | ConvertTo-Json -Depth 5 -Compress
                    break
                }
                ## For completeness, some Extensions have the literal JSON string data. If so,
                'String' {
                    ## Do nothing
                    break
                }

                Default {
                    ## If there was no value, or something else entirely, default to an empty set of parameters
                    $TMAction.methodParams = '[]'
                }
            }
        }

        ## Read the Script Content and add it to the Action Object
        $ScriptContent = (Get-Content -Path $Path -Raw) ?? ''
        Add-Member -InputObject $TMAction -NotePropertyName script -NotePropertyValue ($ScriptContent.ToString() ?? '') -Force
    }

    ## Perform a read of the PS file to capture the ReferenceDesign formatted settings
    else {
        ## Name the Input File
        $Content = Get-Content -Path $Path -Raw

        ## Ignore Empty Files
        if (-Not $Content) {
            return
        }

        $ContentLines = Get-Content -Path $Path

        ## Create Automation Token Variables Parse the Script File
        New-Variable astTokens -Force
        New-Variable astErr -Force
        $ast = [System.Management.Automation.Language.Parser]::ParseInput($Content, [ref]$astTokens, [ref]$astErr)

        ##
        ## Assess the Script Parts to get delineating line numbers
        ##

        ## Locate the Delimiting line
        $ConfigBlockEndLine = $astTokens | `
                Where-Object { $_.Text -like '## End of TM Configuration, Begin Script*' } |`
                Select-Object -First 1 | `
                Select-Object -ExpandProperty Extent | `
                Select-Object -ExpandProperty StartLineNumber

        ## Test to see if the Script is formatted output with Metadata
        if (-not $ConfigBlockEndLine) {

            ## There is no metadata, create the basic object with just the source code
            $ActionConfig = [pscustomobject]@{
                ActionName   = (Get-Item -Path $Path).BaseName
                Description  = ''
                ProviderName = (Get-Item -Path $Path).Directory.BaseName
            }

            ## Create the objects the below constructor expect
            $ConfigBlockEndLine = -1
            $Params = $null
            $TMActionParams = [System.Collections.ArrayList] @()
        } else {

            ##
            ## Read the Script Header to gather the configurations
            ##

            ## Get all of the lines in the header comment
            $TMConfigHeader = 0..$ConfigBlockEndLine | ForEach-Object {

                if ($astTokens[$_].kind -eq 'comment') { $astTokens[$_] }
            } | Select-Object -First 1 | Select-Object -ExpandProperty Text

            ## Create a Properties object that will store the values listed in the header of the script file
            $ActionConfig = [PSCustomObject]@{
            }

            ## Process each line of the Header string
            $TMConfigHeader -split "`n" | ForEach-Object {

                ## Process each line of the comment
                if ($_ -like '*=*') {
                    $k, $v = $_ -split '='
                    $k = $k.Trim() -replace "'", '' -replace '"', ''
                    $v = $v.Trim() -replace "'", '' -replace '"', ''
                    $ActionConfig | Add-Member -NotePropertyName $k -NotePropertyValue $v
                }
            }

            ##
            ## Read the Script Block
            ##

            ## Create a Text StrinBuilder to collect the Script into
            $ActionConfigStringBuilder = New-Object System.Text.StringBuilder

            ## For each line in the Code Block, add it to the Action Script Code StringBuilder
            0..$ConfigBlockEndLine | ForEach-Object {
                $ActionConfigStringBuilder.AppendLine($ContentLines[$_]) | Out-Null
            }
            $ActionConfigScriptString = $ActionConfigStringBuilder.ToString()
            $ActionConfigScriptBlock = [scriptblock]::Create($ActionConfigScriptString)

            ## Invoke the Script Block to create the $Params Object in this scope
            ## this line populates the $Params object from the Action Script
            Invoke-Command -ScriptBlock $ActionConfigScriptBlock -NoNewScope

            ## Collect the Parameters
            $TMActionParams = [System.Collections.ArrayList] @()
            ## Action Parameter Class Definition
            # {
            # "desc": "",
            # "type": "string",
            # "value": "",
            # "context": "DEVICE",
            # "encoded": false,
            # "readonly": false,
            # "required": false,
            # "fieldName": null,
            # "paramName": "IPAddress",
            # "fieldLabel": "IP Address"
            # }

            ## Process the Parameters into Action Params
            foreach ($ParamLabel in $Params.Keys) {

                ## Create a new Params Object to load to the Action
                $NewParamConfig = [PSCustomObject]@{
                    type        = 'string'
                    value       = ''
                    description = ''
                    context     = ''
                    fieldLabel  = ''
                    fieldName   = ''
                    required    = $false
                    encoded     = $false
                    readonly    = $false
                }

                ## Read the existing Params configuration, assembling each additional Paramater option
                $ScriptParamConfig = $Params.$ParamLabel
                $ScriptParamConfig.Keys | ForEach-Object {
                    switch ($_.toLower()) {
                        'value' {
                            $NewParamConfig.value = $ScriptParamConfig[$_]
                            break
                        }
                        'type' {
                            $NewParamConfig.type = $ScriptParamConfig[$_]
                            break
                        }
                        'description' {
                            $NewParamConfig.description = $ScriptParamConfig[$_]
                            break
                        }
                        'desc' {
                            $NewParamConfig.description = $ScriptParamConfig[$_]
                            break
                        }
                        'context' {
                            $NewParamConfig.context = $ScriptParamConfig[$_]
                            break
                        }
                        'fieldlabel' {
                            $NewParamConfig.fieldLabel = $ScriptParamConfig[$_]
                            break
                        }
                        'fieldName' {
                            $NewParamConfig.fieldName = $ScriptParamConfig[$_]
                            break
                        }
                        'required' {
                            $NewParamConfig.required = (ConvertTo-Boolean $ScriptParamConfig[$_])
                            break
                        }
                    }
                }

                ## Add the Parameter Name from the Configuration Object
                Add-Member -InputObject $NewParamConfig -NotePropertyName 'paramName' -NotePropertyValue $ParamLabel
                $TMActionParams.Add($NewParamConfig) | Out-Null
            }

        }

        ## Note where the Action Code is located
        $StartCodeBlockLine = $ConfigBlockEndLine + 1
        $EndCodeBlockLine = $ast[-1].Extent.EndLineNumber

        ## Create a Text StrinBuilder to collect the Script into
        $ActionScriptStringBuilder = New-Object System.Text.StringBuilder

        ## For each line in the Code Block, add it to the Action Script Code StringBuilder
        $StartCodeBlockLine..$EndCodeBlockLine | ForEach-Object {
            $ActionScriptStringBuilder.AppendLine($ContentLines[$_]) | Out-Null
        }

        ## Convert the StringBuilder to a Multi-Line String
        $ActionScriptCode = $ActionScriptStringBuilder.ToString()

        ## If no Parameters were assembled, provide an empty array
        if (-not $TMActionParams) { $TMActionParams = [System.Collections.ArrayList] @() }
        # }


        ## Assemble the Action Object
        $TMAction = [pscustomobject]@{
            id                     = $null
            name                   = $ActionConfig.ActionName
            description            = $ActionConfig.Description
            debugEnabled           = $false

            methodParams           = ($TMActionParams | ConvertTo-Json -Compress)
            script                 = $ActionScriptCode
            reactionScripts        = '{"PRE":"","ERROR":"// Put the task on hold and add a comment with the cause of the error\n task.error( response.stderr )","FINAL":"","FAILED":"","LAPSED":"","STATUS":"// Check the HTTP response code for a 200 OK \n if (response.status == SC.OK) { \n \t return SUCCESS \n } else { \n \t return ERROR \n}","DEFAULT":"// Put the task on hold and add a comment with the cause of the error\n task.error( response.stderr )\n","STALLED":"","SUCCESS":"// Update Asset Fields\nif(response?.data?.assetUpdates){\n\tfor (field in response.data.assetUpdates) {\n \t\tasset.\"${field.key}\" = field.value;\n\t}\n}\ntask.done()"}'

            provider               = @{
                id   = $null
                name = $ActionConfig.ProviderName
            }
            project                = @{
                id   = $null
                name = $null
            }

            remoteCredentialMethod = 'USER_PRIV'
            credential             = $null

            asyncQueue             = $null

            version                = 1
            dateCreated            = Get-Date
            lastUpdated            = Get-Date

            timeout                = 0
            commandLine            = $null
            dictionaryMethodName   = 'Select...'
            callbackMethod         = $null
            connectorMethod        = $null
            pollingStalledAfter    = 0
            pollingInterval        = 0
            pollingLapsedAfter     = 0
            defaultDataScript      = $null
            useWithTask            = 0
            reactionScriptsValid   = 1
            docUrl                 = ''
            isRemote               = $true
            actionType             = 'POWER_SHELL'
            useWithAsset           = 0
            isPolling              = 0
            endpointUrl            = ''
            apiCatalog             = $null
        }
    }

    ## Return the Action Object
    return $TMAction

}

function Invoke-TMActionScript {
    <#
    .SYNOPSIS
    Runs the Script Code in the specified TransitionManager Action
 
    .DESCRIPTION
    Runs the Script Code in the specified TransitionManager Action
 
    .PARAMETER TMSession
    Name of an open TMSession. Use Get-TMSession to get open sessions
 
    .EXAMPLE
    Invoke-TMActionScript -Name 'Ping Remote Machine'
 
    .OUTPUTS
    This command does not output any content
    #>


    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)][String]$TMSession = 'Default',
        [Parameter(Mandatory = $true)][String]$Name,
        [Parameter(Mandatory = $false)][String]$Server = $global:TMSessions[$TMSession].TMServer,
        [Parameter(Mandatory = $false)]$AllowInsecureSSL = $global:TMSessions[$TMSession].AllowInsecureSSL,
        [Parameter(Mandatory = $false)][PSObject]$Project = $global:TMSessions[$TMSession].UserContext.Project,
        [Parameter(Mandatory = $false)][bool]$Api = $false
    )

    ## 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.'
    }

    ## Get the TM Action
    $TMAction = Get-TMAction @PSBoundParameters -Api $Api
    if (-Not $TMAction) {
        throw 'Unable to get the Action'
    }

    ## Try converting and running the script block from the Action
    try {

        ## Create a Temporary Filename
        $TempFile = New-TemporaryFile
        $TempPsFile = $TempFile.FullName -replace '\.tmp', '.ps1'
        Rename-Item -Path $TempFile.FullName -NewName $TempPsFile -Force -Confirm:$false

        $ActionScriptBlock = [scriptblock]::Create($TMAction.Script)
        Set-Content -Value $ActionScriptBlock.ToString() -Path $TempPsFile -Force -Confirm:$false

        ## Invoke the script block, No New Scope to use the available variables,
        ## and with Debug enabled to allow stepping into the file via debugging mode
        try {
            & ($TempPsFile)
        } catch {
            throw $_
        } finally {
            Remove-Item $TempFile, $TempPsFile -Force
        }
    } catch {
        throw $_
    }
}