Create-IntuneRemediation.ps1

<#PSScriptInfo
 
.VERSION 1.1.0
 
.GUID fbee7913-0417-4b0b-8a76-0a4872b6eeba
 
.AUTHOR Jeff Gilbert
 
.COMPANYNAME
 
.COPYRIGHT
 
.TAGS Intune
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
v1.1.0 - 04.03.26 - Updated log cleanup logic to remove ERR logs older than 14 days.
v1.0.0 - 02.12.26 - Original published version. Added a new task to generate an example detection and remediation script pair. Updated the example scripts to include a detection and remediation for Notepad++ installation.
#>


<#
.SYNOPSIS
    Automates the creation of Intune remediation script pairs.
 
.DESCRIPTION
    This script creates a Visual Studio Code workspace for Intune remediation scripts.
    It sets up the necessary folders, tasks, and scripts for creating and managing detection and remediation scripts.
 
.PARAMETER outDir
    The output directory where the workspace and files will be created. Default is the location the IntuneRemediations folder in the My Documents directory.
 
.PARAMETER workSpaceName
    The name of the workspace to be created. Default is "MyRemediation".
 
.EXAMPLE
    .\Create-IntuneRemediation.ps1 -outDir "IntuneRemediations" -workSpaceName "MyRemediation"
    Creates a workspace named "MyRemediation" in the "Documents\IntuneRemediations" directory.
 
.NOTES
    For best results, run this script in a PowerShell terminal with administrative privileges.
    Ensure that the required tools and dependencies are installed and available in the system PATH.
    A scratch directory will be created in the specified output directory for storing test files.
#>


param (
    [Parameter(Mandatory=$false)] [string]$workSpaceName = "MyRemediation",
    [Parameter(Mandatory=$false)] [string]$outDir = "IntuneRemediations"
)

$version = "1.1.0"
Clear-Host
Write-Output "Running Intune Remediation Scripts Workspace Creator v$version ..."
Write-Host `n

$myDocs = [Environment]::GetFolderPath("MyDocuments")
$outDir = Join-Path $myDocs $outDir
$path = Join-Path $outDir $workSpaceName
    if (-not (Test-Path -Path $path)) {
        New-Item -Path $path -ItemType Directory | Out-Null
    } else {
        $rand = Get-Random -Minimum 1 -Maximum 100
        $workSpaceName = $workSpaceName + "-" + $rand
        $path = Join-Path $outDir $workSpaceName
    }

# Create the scripts and scratch folders
New-Item -ItemType Directory -Path $path\scripts | Out-Null
New-Item -ItemType Directory -Path $path\scratch | Out-Null

# Define the folders to include in the workspace
$folders = @(
    @{
        path = $path
    }
)
# Define workspace settings (optional)
$settings = @{
    "editor.tabSize" = 4
    "files.exclude" = @{
        "*.code-workspace" = $true
        ".vscode" = $true
        ".git" = $true
    }
}

$tasks = @{
    version = "2.0.0"
    tasks = @(
        @{        
            label = "Generate example file"
            type = "shell"
            command = "./.vscode/makeExample.ps1"
            problemMatcher = "[]"
        }
    )
}

# Create the workspace JSON structure
$workspace = @{
    folders = $folders
    settings = $settings
    tasks = $tasks
}
# Convert the workspace structure to JSON
$workspaceJson = $workspace | ConvertTo-Json -Depth 10 -Compress

# Write the JSON to the .code-workspace file
Write-Output "Creating the $workSpaceName VS Code Workspace:"

$workspaceFilePath = Join-Path $path "$workSpaceName.code-workspace"
Set-Content -Path $workspaceFilePath -Value $workspaceJson -Encoding UTF8 | Out-Null
Write-Host " WORKSPACE: $workSpaceName.code-workspace created at $outDir"

# Put shortcut in outdir
$WshShell = New-Object -ComObject WScript.Shell
$Shortcut = $WshShell.CreateShortcut("$outDir\$workSpaceName.lnk")
$Shortcut.TargetPath = "$workspaceFilePath"
$Shortcut.Save()

$taskFolderPath = Join-Path $path ".vscode"
# Ensure the .vscode folder exists
    if (-not (Test-Path -Path $taskFolderPath)) {
        New-Item -ItemType Directory -Path $taskFolderPath | Out-Null
        $hide = Get-Item $taskFolderPath -Force
        $hide.attributes = 'Hidden' 
    }

# -------------------------------------------- Create the detection.ps1 file -------------------------------------------
$detectionScript = @"
<#
.SYNOPSIS
    Standard Intune Detection Script Template with CMTrace-compatible logging.
 
.DESCRIPTION
    Provides a reusable framework for Intune Remediations detection scripts,
    including standardized logging, exit code handling, and detection output
    reporting. Detection results are logged to C:\Logs\IR\<LogName>.log.
 
    If one or more failures are detected:
        - The log is renamed to <LogName>_ERR.log
        - Detection details are returned to Intune
        - The remediation script is triggered
 
    If all checks pass:
        - The detection log is deleted
        - Intune reports the device as compliant
 
.PARAMETER logName
    Name used for the detection and remediation log files.
 
.PARAMETER version
    Version number of the detection script for change tracking and troubleshooting.
 
.NOTES
    Author: <Author Name>
    Version: 1.0.0
 
    Exit Codes:
        0 = Compliant (Remediation not required)
        1 = Non-compliant (Remediation required)
 
    Log Files:
        C:\Logs\IR\<LogName>.log
        C:\Logs\IR\<LogName>_ERR.log
 
    Interactive Logging:
        Set `$script:InteractiveLogging to `$true when testing locally.
        Set to `$false when deploying to production.
 
.EXAMPLE
    Detect a missing folder:
 
        if (-not (Test-Path 'C:\SomePath')) {
            `$script:exitCode++
            `$message = '[FAIL] Required path C:\SomePath does not exist'
            `$script:detectionOutput += `$message
            Log `$message
        }
 
#>
 
#------------------------------------------- DO NOT MODIFY OR DELETE THESE FUNCTIONS ---------------------------------------
function Log {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = `$true, Position = 0)] [ValidateNotNullOrEmpty()] [string] `$Message,
        [Parameter(Mandatory = `$false, Position = 1)] [string] `$Component = "Detection",
        [Parameter(Mandatory = `$false, Position = 2)] [ValidateSet(1, 2, 3)] [int] `$Type = 1
    )
  # Capture once to avoid drift between Time/Date
    `$now = Get-Date
    `$Time = `$now.ToString('HH:mm:ss.ffffff', [System.Globalization.CultureInfo]::InvariantCulture)
    `$Date = `$now.ToString('MM-dd-yyyy', [System.Globalization.CultureInfo]::InvariantCulture)
  # Escape message for CMTrace payload (XML-like). SecurityElement.Escape handles &, <, >, " (quotes) safely
    `$escapedMessage = [System.Security.SecurityElement]::Escape(`$Message)
  # Identify if it was a function that called Log and make it the component"
    `$scriptFullPath = `$PSCommandPath ; `$scriptName = Split-Path -Path `$scriptFullPath -Leaf
    `$caller = (Get-PSCallStack)[1].Command
    if (-not (`$caller -eq `$scriptName)){ `$Component = `$caller }
  # Build CMTrace entry (keep empty attributes as per CMTrace spec)
    `$logLine = ('<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="{3}" context="" type="{4}" thread="" file="">' ``
                -f `$escapedMessage, `$Time, `$Date, `$Component, `$Type)
    try { # Write the log entry
        `$dir = Split-Path -Path `$LogFile -Parent # Ensure directory exists
        if (`$dir -and -not (Test-Path -LiteralPath `$dir)) { New-Item -ItemType Directory -Path `$dir -Force | Out-Null }
        Add-Content -LiteralPath `$LogFile -Value `$logLine -Encoding UTF8
    }
    catch { # If writing fails write error
        Write-Error -Message ("Failed to write log to '{0}': {1}" -f `$LogFile, `$_.Exception.Message)
        return
    }
  # Write all log lines to console for interactive testing/transcript capture
    if (`$script:InteractiveLogging) {
        switch (`$Type) {
            1 { Write-Host `$Message -ForegroundColor Gray }
            2 { Write-Host `$Message -ForegroundColor Yellow }
            3 { Write-Host `$Message -ForegroundColor Red }
            default { Write-Host `$Message }
        }
    }
}
 
function Start-Log {
# When the detection script runs, a log file will be generated at C:\Logs\IR\<`$logName>.log.
[CmdletBinding()]
param (
    [Parameter(Mandatory = `$true)]
    [ValidateNotNullOrEmpty()]
    [string]`$logName
)
    try {
        `$script:logPath = "`$env:SystemDrive\Logs\IR" # This path is pulled when Intune collects diagnostics
        `$script:logFile = Join-Path `$(`$script:logPath) "`$logName.log"
        `$script:errFile = Join-Path `$(`$script:logPath) `$logName"_ERR.log"
        Log "----------------------------- BEGIN `$(`$logName.ToUpper()) DETECTION -----------------------------" # Initialize the log file
        }
    catch {
        Write-Output "ERROR: `$(`$_.Exception.Message)"
    }
}
 
function Exit-Result {
# If there are no issues, the detection log file will automatically be deleted. If issues are detected, the file
# will be renamed <logName>_ERR.log and written to by the remediation script. If subsequent
# remediation runs all pass, the ERR log file will be deleted after it has aged over 14 days.
    try {
      # Remediation exit codes for Intune reporting
        if (`$script:exitCode -ge 1) {
          Log "Non-zero exit code determined; remediation is necessary."
        # End the detection log with a footer to indicate the end of the detection section
          Log "------------------------------ END `$(`$logName.ToUpper()) DETECTION ------------------------------" # End detection section of the log file
        # Create ERR log file
          If (!(Test-Path `$script:errFile )){
                Rename-Item `$script:logfile `$script:errFile -Force
            }
            else { Remove-Item `$script:errFile -Force -ErrorAction SilentlyContinue
                Rename-Item `$script:logfile `$script:errFile -Force
            }
            Write-Output (`$detectionOutput -join " | ") # This is what's displayed in the Intune admin center's detection output
            Exit 1 # Remediation needed
        } else {
            Write-Output "[PASS] All checks successful" # This is what's displayed in the Intune admin center's detection output
          # End the detection log with a footer to indicate the end of the detection section
            Log "------------------------------ END `$(`$logName.ToUpper()) DETECTION ------------------------------" # End detection section of the log file
       
          # Clean up old ERR logs
            if (Test-Path `$script:errFile){
                `$retainDays = 14
                `$cutOff = (Get-Date).AddDays(-`$retainDays)
                `$errAge = (Get-Item `$errFile).LastWriteTime
            if ( `$errAge -lt `$cutOff ){ Remove-Item `$script:errFile -Force -ErrorAction SilentlyContinue }
            }
            Remove-Item `$script:logFile -Force -ErrorAction SilentlyContinue # No error = no log
            Exit 0 # Remediation not needed
        }
    }catch{
        Write-Output "ERROR: `$(`$_.Exception.Message)"
    }
}
 
#----------------------------------------------------------- Prep ----------------------------------------------------------
# If we are running as a 32-bit process on an x64 system, re-launch as a 64-bit process first
if ("`$env:PROCESSOR_ARCHITEW6432" -ne "ARM64") {
    if (Test-Path "`$(`$env:WINDIR)\SysNative\WindowsPowerShell\v1.0\powershell.exe") {
        & "`$(`$env:WINDIR)\SysNative\WindowsPowerShell\v1.0\powershell.exe" -ExecutionPolicy bypass -NoProfile -File "`$PSCommandPath"
        Exit `$lastexitcode
        }
    }
 
#------------------------------------------------------- Begin Script -------------------------------------------------------
# ONLY CHANGE THE NAME OF THE LOG FILE (`$logName) AND SCRIPT VERSION (`$version)IN THE FOLLOWING LINES
# These are required for proper logging and exit code handling
`$script:exitCode = 0 # Variable holding the number of failures detected
`$script:detectionOutput = @() # Arrary holding remediation detection output to display in Intune admin center
# -------------------------------------------------------------------
`$logName = "MyRemediation" # Change this to the name you want for your detection script.
`$version = "1.0.0" # Change this to the version of your detection script.
# -------------------------------------------------------------------
`$script:InteractiveLogging = `$true # Set to `$true to enable interactive logging for testing. Set to `$false for production use.
Start-Log `$logName # Initialize the detection log file
Log "Script version: `$version" -Type 1
  
#----------------------------------------------------- Helper Functions ----------------------------------------------------
# Add any additional functions you want to use in your detection script here
 
 
 
#---------------------------------------------------------------------------------------------------------------------------
 
try {
  # Detection should only evaluate state. Do not modify system configuration in this script.
  # Add your detection logic below. If a failure is detected, increment the exit code and add a message to the detection output array.
   
  # Example:
    # if (-not (Test-Path "C:\SomePath")) {
    # `$script:exitCode++
    # `$message = "[FAIL] Required path C:\SomePath does not exist"
    # `$script:detectionOutput += `$message
    # Log `$message
    # }
    #
    # if (-not (Test-Path "C:\SomePath1")) {
    # `$script:exitCode++
    # `$message = "[FAIL] Required path C:\SomePath1 does not exist"
    # `$script:detectionOutput += `$message
    # Log `$message
    # }
 
  # If the detection script determines that remediation is necessary, the remediation script will be run
}
catch {
  # DO NOT MODIFY OR DELETE THE FOLLOWING LINES
    Log "ERROR: `$(`$_.Exception.Message)" -Type 3
}
 
# DO NOT MODIFY OR DELETE THE FOLLOWING LINE. It is required for proper logging and exit code handling.
Exit-Result
 
"@

$detectionScript | Out-File -FilePath $path\scripts\$workSpaceName"_detection.ps1" -Encoding UTF8 -Force
Write-Output " TEMPLATE: Detection script created"

#$logFileName = $workspacename + "_remediation.log"
$remediationScript = @"
<#
.SYNOPSIS
    Standard Intune Remediation Script Template with CMTrace-compatible logging.
 
.DESCRIPTION
    Provides a reusable framework for Intune Remediations remediation scripts,
    including standardized logging, error handling, and 64-bit PowerShell
    relaunch support.
 
    The remediation script appends actions and results to:
        C:\Logs\IR\<LogName>_ERR.log
 
    This log is created by the associated detection script when a
    non-compliant condition is found. After remediation completes,
    Intune automatically reruns the detection script to verify that
    the issue has been resolved.
 
.PARAMETER logName
    Name of the remediation log file. Must exactly match the LogName
    used by the associated detection script.
 
.PARAMETER version
    Version number of the remediation script for change tracking and
    troubleshooting.
 
.NOTES
    Author: <Author Name>
    Version: 1.0.0
 
    Log File:
        C:\Logs\IR\<LogName>_ERR.log
 
    Exit Code:
        0 = Remediation completed
 
    The logName value MUST BE IDENTICAL to the detection script logName.
     
    Interactive Logging:
        Set `$script:InteractiveLogging to `$true for local testing.
        Set to `$false for production deployments.
 
.EXAMPLE
    Remediate a missing folder:
 
        if (-not (Test-Path 'C:\SomePath')) {
            New-Item -Path 'C:\SomePath' -ItemType Directory -Force
            Log 'Created folder: C:\SomePath'
        }
 
#>
 
#------------------------------------------- DO NOT MODIFY OR DELETE THESE FUNCTIONS ---------------------------------------
function Log {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = `$true, Position = 0)] [ValidateNotNullOrEmpty()] [string] `$Message,
        [Parameter(Mandatory = `$false, Position = 1)] [string] `$Component = "Remediation",
        [Parameter(Mandatory = `$false, Position = 2)] [ValidateSet(1, 2, 3)] [int] `$Type = 1
    )
  # Capture once to avoid drift between Time/Date
    `$now = Get-Date
    `$Time = `$now.ToString('HH:mm:ss.ffffff', [System.Globalization.CultureInfo]::InvariantCulture)
    `$Date = `$now.ToString('MM-dd-yyyy', [System.Globalization.CultureInfo]::InvariantCulture)
  # Escape message for CMTrace payload (XML-like). SecurityElement.Escape handles &, <, >, " (quotes) safely
    `$escapedMessage = [System.Security.SecurityElement]::Escape(`$Message)
  # Identify if it was a function that called Log and make it the component"
    `$scriptFullPath = `$PSCommandPath ; `$scriptName = Split-Path -Path `$scriptFullPath -Leaf
    `$caller = (Get-PSCallStack)[1].Command
    if (-not (`$caller -eq `$scriptName)){ `$Component = `$caller }
  # Build CMTrace entry (keep empty attributes as per CMTrace spec)
    `$logLine = ('<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="{3}" context="" type="{4}" thread="" file="">' ``
                -f `$escapedMessage, `$Time, `$Date, `$Component, `$Type)
    try { # Write the log entry
        `$dir = Split-Path -Path `$logFile -Parent # Ensure directory exists
        if (`$dir -and -not (Test-Path -LiteralPath `$dir)) { New-Item -ItemType Directory -Path `$dir -Force | Out-Null }
        Add-Content -LiteralPath `$logFile -Value `$logLine -Encoding UTF8
    }
    catch { # If writing fails write error
        Write-Error -Message ("Failed to write log to '{0}': {1}" -f `$logFile, `$_.Exception.Message)
        return
    }
  # Write all log lines to console for interactive testing/transcript capture
    if (`$script:InteractiveLogging) {
        switch (`$Type) {
            1 { Write-Host `$Message -ForegroundColor Gray }
            2 { Write-Host `$Message -ForegroundColor Yellow }
            3 { Write-Host `$Message -ForegroundColor Red }
            default { Write-Host `$Message }
        }
    }
}
 
function Start-Log {
# When the detection script runs, a log file will be generated at C:\Logs\IR\<`$logName>.log.
[CmdletBinding()]
param (
    [Parameter(Mandatory = `$true)]
    [ValidateNotNullOrEmpty()]
    [string]`$logName
)
    try {
        `$script:logPath = "`$env:SystemDrive\Logs\IR" # This path is pulled when Intune collects diagnostics
        # `$script:logFile = Join-Path `$(`$script:logPath) "`$logName.log"
        `$script:logFile = Join-Path `$(`$script:logPath) `$logName"_ERR.log" # This is the remediation log file created if remediation is needed.
        # Start the remediation logging with a header to indicate the beginning of the remediation section
        Log "----------------------------- BEGIN `$(`$logName.ToUpper()) REMEDIATION -----------------------------" # Initialize the log file
        }
    catch {
        Write-Output "ERROR: `$(`$_.Exception.Message)"
    }
}
 
#----------------------------------------------------------- Prep ----------------------------------------------------------
# If we are running as a 32-bit process on an x64 system, re-launch as a 64-bit process first
if ("`$env:PROCESSOR_ARCHITEW6432" -ne "ARM64") {
    if (Test-Path "`$(`$env:WINDIR)\SysNative\WindowsPowerShell\v1.0\powershell.exe") {
        & "`$(`$env:WINDIR)\SysNative\WindowsPowerShell\v1.0\powershell.exe" -ExecutionPolicy bypass -NoProfile -File "`$PSCommandPath"
        Exit `$lastexitcode
        }
    }
 
#------------------------------------------------------- Begin Script -------------------------------------------------------
# `$logName SHOULD BE THE SAME NAME AS THE DETECTION LOG NAME (_ERR will be added to path when logging)
# -------------------------------------------------------------------
`$logName = "MyRemediation" # Change this to EXACTLY MATCH the name of your detection script.
`$version = "1.0.0" # Change this to the version of your remediation script.
# -------------------------------------------------------------------
`$script:InteractiveLogging = `$true # Set to `$true to enable interactive logging for testing. Set to `$false for production use.
Start-Log `$logName # Initialize the remediation log file
Log "Script version: `$version" -Type 1
 
#----------------------------------------------------- Helper Functions ----------------------------------------------------
# Add any additional functions you want to use in your detection script here
 
 
 
#---------------------------------------------------------------------------------------------------------------------------
 
try {
    `$ErrorActionPreference = 'Stop' # Ensures try { } catch { } works reliably
  # Remediation must be idempotent. Running this script multiple times should be safe.
  # Add your remediation logic below. Add messages to the log file as needed.
 
  # Example:
    # if (Test-Path -Path "C:\SomePath") {
    # Log "Folder exists: C:\SomePath"
    # }
    # else {
    # Log "Folder does not exist: C:\SomePath"
    # New-Item -Path "C:\SomePath" -ItemType Directory -Force | Out-Null
    # Log " Folder created: C:\SomePath"
    #}
    #
    # if (Test-Path -Path "C:\SomePath1") {
    # Log "Folder exists: C:\SomePath1"
    # }
    # else {
    # Log "Folder does not exist: C:\SomePath1"
    # New-Item -Path "C:\SomePath1" -ItemType Directory -Force | Out-Null
    # Log " Folder created: C:\SomePath1"
    #}
 
}
catch {
    Log "ERROR: `$(`$_.Exception.Message)" -Type 3
}
finally {
  # DO NOT MODIFY OR DELETE THE FOLLOWING LINES
  # End the remediation log with a footer to indicate the end of the remediation section
  Log "----------------------------- END `$(`$logName.ToUpper()) REMEDIATION -----------------------------"
}
  
# If remediation is successful, the detection script will be re-run by Intune to verify that the issue has been resolved.
 
Exit 0
 
"@

$remediationScript | Out-File -FilePath $path\scripts\$workSpaceName"_remediation.ps1" -Encoding UTF8 -Force
Write-Output " TEMPLATE: Remediation script created"

#--------------------------------------------- Create Example File --------------------------------------------
$makeApp = @"
`$makeExample = @"
# detection.ps1
# Detect if Notepad++ is installed
 
```$AppName = "Notepad++"
```$Installed = Get-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | Where-Object { ```$_.DisplayName -like "*```$AppName*" }
 
if (```$Installed) {
    Write-Output "```$AppName is installed."
    exit 0 # Detection: compliant
} else {
    Write-Output "```$AppName is NOT installed."
    exit 1 # Detection: non-compliant
}
 
# remediation.ps1
# Install Notepad++ if not present
 
```$AppName = "Notepad++"
```$Installed = Get-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | Where-Object { ```$_.DisplayName -like "*```$AppName*" }
 
if (-not ```$Installed) {
    ```$InstallerUrl = "https://github.com/notepad-plus-plus/notepad-plus-plus/releases/latest/download/npp.8.6.7.Installer.x64.exe"
    ```$InstallerPath = "```$env:TEMP\npp_installer.exe"
 
    Invoke-WebRequest -Uri ```$InstallerUrl -OutFile ```$InstallerPath
 
    Start-Process -FilePath ```$InstallerPath -ArgumentList "/S" -Wait
 
    Remove-Item ```$InstallerPath -Force
}
 
`"@
`$makeExample | Out-File -FilePath "$path\scripts\examples.ps1" -Encoding UTF8 -Force
"@

$makeApp | Out-File -FilePath "$taskFolderPath\makeExample.ps1" -Encoding UTF8 -Force
Write-Output " TASK: Generate example file created"
Write-Host `n

# --------------------------------------------------- Finish up -------------------------------------------------------------
Write-Output "VS Code Workspace creation complete!"
Write-Output `n
$openWorkspace = Read-Host "Do you want to open the workspace in VS Code now? [Y or N]"
if ($openWorkspace -eq "Y" -or $openWorkspace -eq "y") {
    try {
        #invoke-item $workspaceFilePath -ErrorAction SilentlyContinue
        code $workspaceFilePath #-NoNewWindow -ErrorAction SilentlyContinue
        Write-Output "Workspace opened in VS Code."
        Exit
    }
    catch {
        Write-Host "Unable to open VS Code. Please check if VS Code is installed."
        # Uncomment the line below to see the error details
        # Write-Output "Error: $_"
    }
    
    $portable = Read-Host "Do you want to use VSCode Portable to open the workspace? [Y or N]"
        if ($portable -eq "Y" -or $portable -eq "y") {
            try {
                #If VS Code portable is already installed, use it.
                $vsCodePortablePath =  "$outDir\.VSCodePortable\Code.exe"
                if (Test-Path $vsCodePortablePath){
                    Start-Process -FilePath $vsCodePortablePath -ArgumentList """$workspaceFilePath"""
                    Write-Output "Workspace opened in VS Code Portable."
                    Exit
                }
            }
            catch {
            #VS Code portable is NOT installed
            $vsCodePortablePath =  "$outDir\.VSCodePortable\Code.exe"             
            $vscodePath = Join-Path $outDir ".VSCodePortable"
            if (-not (Test-Path -Path $vscodePath)) {
                New-Item -ItemType Directory -Path $vscodePath | Out-Null
            }
            
            # Find processor architecture for download link https://code.visualstudio.com/download
            $architecture = (Get-WMIObject -Class Win32_Processor).Architecture
            if ($architecture -eq 9) { #x64
                $vscodeZip = "https://code.visualstudio.com/sha/download?build=stable&os=win32-x64-archive"
            } elseif ($architecture -eq 5) { #arm64
                $vscodeZip = "https://code.visualstudio.com/sha/download?build=stable&os=win32-arm64-archive"
            } else {
                Write-Host "Unsupported architecture: $architecture" -ForegroundColor Red
            }    
                
            # Download VS Code portable .zip
            $vscodeFile = "VSCodePortable.zip"
            $vscodeOutFile = Join-Path $vscodePath $vscodeFile
            Write-Output " Downloading and extracting VS Code Portable..."
            Invoke-WebRequest -Uri $vscodeZip -OutFile $vscodeOutFile -UseBasicParsing

            if (-not (Test-Path -Path $vscodeOutFile)) {
                Write-Error "Failed to download VS Code Portable. Please check your internet connection."
                Write-Warning "Direct download link is $vscodeZip"
            }else {
            # Extract the downloaded .zip file
                Expand-Archive -Path $vscodeOutFile -DestinationPath $vscodePath -Force
            
            # Delete .zip file after extraction
                Remove-Item -Path $vscodeOutFile -Force
                Write-Output "VS Code Portable downloaded to $vscodePath"
            
            # Open workspace?
            $openWorkspace = Read-Host "Do you want to open the workspace in VS Code now? [Y or N]"
            if ($openWorkspace  -eq "Y" -or $openWorkspace  -eq "y") {
                $vscode = Join-Path -Path $vscodePath -ChildPath "\Code.exe" 
                start-process $vscode -ArgumentList """$workspaceFilePath""" -NoNewWindow -ErrorAction SilentlyContinue
                Write-Output "Workspace opened in VS Code."
                Write-Output "You can also open the workspace later by double-clicking the $Shortcut created in $outDir"
                Write-Output `n
                Start-Sleep -seconds 10
                Exit
            }
        }           
    }
}
} else {
    Write-Output "You chose not to open the workspace." `n
    Write-Output "You can open the workspace later by double-clicking the $workSpaceName workspace shortcut created in $outDir" `n
    Write-Output "Good-bye." `n
}

Exit