Skip to main content

Automated Uninstall Script

While the WhatPulse Professional desktop application can be removed manually via standard operating system methods, automating the uninstallation process can be beneficial for large-scale deployments or when managing multiple devices. Below is a sample script that can be used to silently uninstall the WhatPulse Professional application from Windows.

Usage

  1. Save the script below as cleanup-whatpulse.ps1.
  2. Open PowerShell with administrative privileges on a test machine.
  3. Run the script using the command: .\cleanup-whatpulse.ps1
  4. Optionally, you can add parameters:
    • -InstallDir "C:\Custom\Path\To\WhatPulse": Specify a custom installation directory if WhatPulse is not installed in the default location (Program Files).
    • -SkipAppData: Preserve user data and database located in %LOCALAPPDATA%.
    • -WhatIf: Perform a dry run to see what would be removed without making any changes.
  5. Review the output for any errors or warnings.
  6. Deploy the script across your organization using your preferred software deployment tool (e.g., SCCM, Intune, PDQ Deploy).

Script

<#
.SYNOPSIS
Complete cleanup script for WhatPulse and WhatPulse-Professional.

.DESCRIPTION
Removes WhatPulse or WhatPulse-Professional without using the maintenance tool.
Cleans up processes, registry entries, scheduled tasks, application data,
installation directories, and shortcuts.

Run as Administrator for complete cleanup.

.PARAMETER InstallDir
Custom installation directory path. If not specified, checks default locations
for both WhatPulse and WhatPulse-Professional.

.PARAMETER SkipAppData
Preserve user data and database in %LOCALAPPDATA%.

.PARAMETER WhatIf
Dry run - shows what would be removed without making changes.

.EXAMPLE
.\cleanup-whatpulse.ps1
Default cleanup checking all standard locations.

.EXAMPLE
.\cleanup-whatpulse.ps1 -WhatIf
Preview what will be removed without making changes.

.EXAMPLE
.\cleanup-whatpulse.ps1 -InstallDir "D:\Apps\WhatPulse"
Clean up a custom installation location.

.EXAMPLE
.\cleanup-whatpulse.ps1 -SkipAppData
Remove program but preserve user data and database.

.NOTES
Author: WhatPulse
Requires: PowerShell 5.1 or later
Run as: Administrator (recommended)
#>

#Requires -Version 5.1

[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(HelpMessage = "Custom installation directory path")]
[string]$InstallDir = "",

[Parameter(HelpMessage = "Preserve user data and database")]
[switch]$SkipAppData
)

Set-StrictMode -Version Latest
$ErrorActionPreference = "Continue"

#region Configuration

# Application variants to clean up
$script:AppVariants = @("WhatPulse", "WhatPulse-Professional")

# Process names to terminate (watchdog first, then main apps, then maintenance tool)
$script:ProcessNames = @(
"whatpulse-watchdog", # Kill watchdog FIRST to prevent it from restarting the main process
"WhatPulse",
"WhatPulse-Professional",
"WhatPulseMaintenanceTool"
)

# Registry key names for startup entries (case variations)
$script:StartupKeyNames = @("WhatPulse", "whatpulse", "WhatPulse-Professional", "whatpulse-professional")

# Track cleanup results for summary
$script:CleanupResults = @{
ProcessesStopped = @()
RegistryKeysRemoved = @()
TasksRemoved = @()
DirectoriesRemoved = @()
ShortcutsRemoved = @()
Errors = @()
}

#endregion

#region Helper Functions

function Write-Status {
<#
.SYNOPSIS
Writes a colored status message to the console.
#>
param(
[Parameter(Mandatory)]
[string]$Message,

[ValidateSet("Info", "Success", "Warning", "Error", "Section")]
[string]$Type = "Info"
)

$config = switch ($Type) {
"Success" { @{ Color = "Green"; Prefix = "[OK] " } }
"Warning" { @{ Color = "Yellow"; Prefix = "[WARN] " } }
"Error" { @{ Color = "Red"; Prefix = "[ERROR]" } }
"Section" { @{ Color = "Magenta"; Prefix = "" } }
default { @{ Color = "Cyan"; Prefix = "[INFO] " } }
}

Write-Host "$($config.Prefix) $Message" -ForegroundColor $config.Color
}

function Write-SectionHeader {
<#
.SYNOPSIS
Writes a section header for visual separation.
#>
param([Parameter(Mandatory)][string]$Title)

Write-Host ""
Write-Status "--- $Title ---" -Type Section
}

function Test-IsAdmin {
<#
.SYNOPSIS
Checks if the current session has administrator privileges.
#>
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]$identity
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Test-SafePath {
<#
.SYNOPSIS
Validates that a path is safe to delete (not a system directory).
.DESCRIPTION
Returns $true if the path is safe to delete, $false otherwise.
A path is safe if it contains a WhatPulse variant name and is not a protected system path.
#>
param([Parameter(Mandatory)][string]$Path)

if ([string]::IsNullOrWhiteSpace($Path)) {
return $false
}

# Normalize the path for comparison
$normalizedPath = $Path.TrimEnd('\', '/').ToLower()

# List of protected system paths that should never be deleted
$protectedPaths = @(
$env:SystemRoot,
$env:ProgramFiles,
${env:ProgramFiles(x86)},
$env:ProgramData,
$env:USERPROFILE,
$env:LOCALAPPDATA,
$env:APPDATA,
$env:SystemDrive
) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.TrimEnd('\', '/').ToLower() }

# Check if path exactly matches a protected path (can't delete the root of these)
if ($protectedPaths -contains $normalizedPath) {
return $false
}

# Verify path contains a WhatPulse-related folder name
$containsWhatPulse = $false
foreach ($variant in $script:AppVariants) {
if ($normalizedPath -like "*\$($variant.ToLower())" -or $normalizedPath -like "*\$($variant.ToLower())\*") {
$containsWhatPulse = $true
break
}
}

return $containsWhatPulse
}

function Remove-ItemSafely {
<#
.SYNOPSIS
Safely removes a file or directory with validation and error handling.
#>
param(
[Parameter(Mandatory)]
[string]$Path,

[Parameter(Mandatory)]
[string]$Description
)

if ([string]::IsNullOrWhiteSpace($Path)) {
return $false
}

if (-not (Test-Path $Path)) {
return $false
}

# Security check: validate the path is safe to delete
if (-not (Test-SafePath -Path $Path)) {
Write-Status "Skipping path (safety check): $Path" -Type Warning
$script:CleanupResults.Errors += "Safety check failed: $Path"
return $false
}

Write-Status "Removing $Description`: $Path"

if ($PSCmdlet.ShouldProcess($Path, "Remove $Description")) {
try {
Remove-Item -Path $Path -Recurse -Force -ErrorAction Stop
$script:CleanupResults.DirectoriesRemoved += $Path
Write-Status "Removed: $Path" -Type Success
return $true
}
catch {
Write-Status "Failed: $($_.Exception.Message)" -Type Warning
$script:CleanupResults.Errors += "Failed to remove ${Path}: $($_.Exception.Message)"

# Attempt individual file removal as fallback
try {
Get-ChildItem -Path $Path -Recurse -Force -ErrorAction SilentlyContinue |
Sort-Object -Property FullName -Descending |
ForEach-Object {
Remove-Item -Path $_.FullName -Force -ErrorAction SilentlyContinue
}
Remove-Item -Path $Path -Force -ErrorAction SilentlyContinue

if (-not (Test-Path $Path)) {
$script:CleanupResults.DirectoriesRemoved += $Path
Write-Status "Removed (fallback): $Path" -Type Success
return $true
}
}
catch {
# Fallback also failed, already logged above
}
return $false
}
}
return $true
}

function Get-InstallationPaths {
<#
.SYNOPSIS
Returns a list of potential installation paths to check.
#>
param([string]$CustomPath)

$paths = [System.Collections.ArrayList]::new()

if (-not [string]::IsNullOrWhiteSpace($CustomPath)) {
# User specified a custom path
[void]$paths.Add($CustomPath)
}
else {
# Check default locations for all app variants
foreach ($variant in $script:AppVariants) {
# Program Files
[void]$paths.Add((Join-Path $env:ProgramFiles $variant))

# Program Files (x86) - only if different from Program Files
if ($env:ProgramFiles -ne ${env:ProgramFiles(x86)}) {
[void]$paths.Add((Join-Path ${env:ProgramFiles(x86)} $variant))
}

# User-specific Programs folder (Qt Installer Framework default for per-user install)
[void]$paths.Add((Join-Path $env:LOCALAPPDATA "Programs\$variant"))
}
}

return $paths | Select-Object -Unique
}

function Get-AppDataPaths {
<#
.SYNOPSIS
Returns application data paths for all variants.
#>
$paths = [System.Collections.ArrayList]::new()
foreach ($variant in $script:AppVariants) {
[void]$paths.Add((Join-Path $env:LOCALAPPDATA $variant))
}
return $paths | Select-Object -Unique
}

#endregion

#region Cleanup Functions

function Stop-WhatPulseProcesses {
<#
.SYNOPSIS
Terminates all WhatPulse-related processes.
Kills watchdog first to prevent automatic restart of main process.
#>
Write-SectionHeader "Stopping Processes"

$foundProcesses = $false

# Process names are ordered: watchdog first, then main apps
foreach ($processName in $script:ProcessNames) {
$processes = Get-Process -Name $processName -ErrorAction SilentlyContinue

if ($processes) {
$foundProcesses = $true
foreach ($proc in $processes) {
Write-Status "Found: $($proc.Name) (PID: $($proc.Id))"

if ($PSCmdlet.ShouldProcess("$($proc.Name) (PID: $($proc.Id))", "Stop process")) {
try {
Stop-Process -Id $proc.Id -Force -ErrorAction Stop
$script:CleanupResults.ProcessesStopped += "$($proc.Name) (PID: $($proc.Id))"
Write-Status "Stopped: $($proc.Name)" -Type Success
}
catch {
Write-Status "Failed to stop $($proc.Name): $($_.Exception.Message)" -Type Warning
$script:CleanupResults.Errors += "Failed to stop process: $($proc.Name)"
}
}
}
}
}

if (-not $foundProcesses) {
Write-Status "No WhatPulse processes running" -Type Info
}
else {
# Wait for processes to fully terminate
Start-Sleep -Seconds 2
}
}

function Remove-RegistryEntries {
<#
.SYNOPSIS
Removes all WhatPulse-related registry entries.
#>
Write-SectionHeader "Removing Registry Entries"

# Startup entries in Run keys
Write-Status "Checking startup entries..."
$runKeyPaths = @(
"HKCU:\Software\Microsoft\Windows\CurrentVersion\Run",
"HKLM:\Software\Microsoft\Windows\CurrentVersion\Run"
)

foreach ($runKeyPath in $runKeyPaths) {
if (-not (Test-Path $runKeyPath)) {
continue
}

foreach ($keyName in $script:StartupKeyNames) {
try {
$keyExists = Get-ItemProperty -Path $runKeyPath -Name $keyName -ErrorAction SilentlyContinue
if ($null -ne $keyExists) {
Write-Status "Found startup entry: $keyName in $runKeyPath"

if ($PSCmdlet.ShouldProcess("$runKeyPath\$keyName", "Remove registry value")) {
Remove-ItemProperty -Path $runKeyPath -Name $keyName -ErrorAction Stop
$script:CleanupResults.RegistryKeysRemoved += "$runKeyPath\$keyName"
Write-Status "Removed: $keyName" -Type Success
}
}
}
catch {
# Silently ignore if key doesn't exist
if ($_.Exception.Message -notlike "*does not exist*" -and $_.Exception.Message -notlike "*Cannot find path*") {
Write-Status "Failed to remove $keyName`: $($_.Exception.Message)" -Type Warning
}
}
}
}

# Application settings registry keys
Write-Status "Checking application settings..."
$appRegPaths = [System.Collections.ArrayList]::new()
foreach ($variant in $script:AppVariants) {
[void]$appRegPaths.Add("HKCU:\Software\$variant")
[void]$appRegPaths.Add("HKLM:\Software\$variant")
[void]$appRegPaths.Add("HKLM:\Software\WOW6432Node\$variant")
}
# Also check org domain key
[void]$appRegPaths.Add("HKCU:\Software\whatpulse.org")

foreach ($regPath in $appRegPaths) {
if (Test-Path $regPath) {
Write-Status "Found settings key: $regPath"

if ($PSCmdlet.ShouldProcess($regPath, "Remove registry key")) {
try {
Remove-Item -Path $regPath -Recurse -Force -ErrorAction Stop
$script:CleanupResults.RegistryKeysRemoved += $regPath
Write-Status "Removed: $regPath" -Type Success
}
catch {
Write-Status "Failed to remove $regPath`: $($_.Exception.Message)" -Type Warning
$script:CleanupResults.Errors += "Failed to remove registry key: $regPath"
}
}
}
}

# Uninstall entries (Add/Remove Programs)
Write-Status "Checking Add/Remove Programs entries..."
$uninstallPaths = @(
"HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall",
"HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall",
"HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall"
)

foreach ($uninstallPath in $uninstallPaths) {
if (-not (Test-Path $uninstallPath)) {
continue
}

$subKeys = Get-ChildItem -Path $uninstallPath -ErrorAction SilentlyContinue
if ($null -eq $subKeys) {
continue
}

foreach ($subKey in $subKeys) {
try {
$props = Get-ItemProperty -Path $subKey.PSPath -ErrorAction SilentlyContinue
if ($null -eq $props) {
continue
}

# Safely check for DisplayName property
$displayName = $null
if ($props.PSObject.Properties.Name -contains 'DisplayName') {
$displayName = $props.DisplayName
}

if ([string]::IsNullOrWhiteSpace($displayName)) {
continue
}

# Check if this entry is for any WhatPulse variant
$isWhatPulse = $false
foreach ($variant in $script:AppVariants) {
if ($displayName -like "*$variant*") {
$isWhatPulse = $true
break
}
}

if ($isWhatPulse) {
Write-Status "Found uninstall entry: $displayName"

if ($PSCmdlet.ShouldProcess($subKey.PSPath, "Remove uninstall entry for '$displayName'")) {
Remove-Item -Path $subKey.PSPath -Recurse -Force -ErrorAction Stop
$script:CleanupResults.RegistryKeysRemoved += "Uninstall: $displayName"
Write-Status "Removed: $displayName" -Type Success
}
}
}
catch {
# Silently continue on individual entry errors
continue
}
}
}
}

function Remove-ScheduledTasks {
<#
.SYNOPSIS
Removes WhatPulse scheduled tasks from Task Scheduler.
#>
Write-SectionHeader "Removing Scheduled Tasks"

$foundTasks = $false

foreach ($variant in $script:AppVariants) {
try {
$task = Get-ScheduledTask -TaskName $variant -ErrorAction SilentlyContinue

if ($null -ne $task) {
$foundTasks = $true
Write-Status "Found scheduled task: $variant"

if ($PSCmdlet.ShouldProcess($variant, "Remove scheduled task")) {
Unregister-ScheduledTask -TaskName $variant -Confirm:$false -ErrorAction Stop
$script:CleanupResults.TasksRemoved += $variant
Write-Status "Removed: $variant" -Type Success
}
}
}
catch {
# Only report actual errors, not "task not found"
if ($_.Exception.Message -notlike "*No MSFT_ScheduledTask objects found*" -and
$_.Exception.Message -notlike "*cannot find*") {
Write-Status "Failed to remove task $variant`: $($_.Exception.Message)" -Type Warning
$script:CleanupResults.Errors += "Failed to remove scheduled task: $variant"
}
}
}

if (-not $foundTasks) {
Write-Status "No scheduled tasks found" -Type Info
}
}

function Remove-AppDataDirectories {
<#
.SYNOPSIS
Removes application data directories (user data, database, settings).
#>
Write-SectionHeader "Removing Application Data"

if ($SkipAppData) {
Write-Status "Skipping application data (preserving user data)" -Type Warning
return
}

$appDataPaths = Get-AppDataPaths
$foundAppData = $false

foreach ($appDataPath in $appDataPaths) {
if (Test-Path $appDataPath) {
$foundAppData = $true
Remove-ItemSafely -Path $appDataPath -Description "application data"
}
}

if (-not $foundAppData) {
Write-Status "No application data directories found" -Type Info
}
}

function Remove-InstallationDirectories {
<#
.SYNOPSIS
Removes installation directories.
#>
Write-SectionHeader "Removing Installation Directories"

$installPaths = Get-InstallationPaths -CustomPath $InstallDir
$foundInstall = $false

Write-Status "Checking installation paths..."

foreach ($installPath in $installPaths) {
if (Test-Path $installPath) {
$foundInstall = $true
Remove-ItemSafely -Path $installPath -Description "installation directory"
}
}

if (-not $foundInstall) {
Write-Status "No installation directories found" -Type Info
Write-Status "Checked locations:" -Type Info
foreach ($path in $installPaths) {
Write-Host " - $path" -ForegroundColor Gray
}
}
}

function Remove-StartMenuShortcuts {
<#
.SYNOPSIS
Removes Start Menu folders and shortcuts.
#>
Write-SectionHeader "Removing Start Menu Shortcuts"

$foundShortcuts = $false

# Start Menu program folders
foreach ($variant in $script:AppVariants) {
$startMenuPaths = @(
(Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\$variant"),
(Join-Path $env:ProgramData "Microsoft\Windows\Start Menu\Programs\$variant")
)

foreach ($menuPath in $startMenuPaths) {
if (Test-Path $menuPath) {
$foundShortcuts = $true
Remove-ItemSafely -Path $menuPath -Description "Start Menu folder"
}
}
}

# Individual shortcut files
$shortcutLocations = @(
(Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs"),
(Join-Path $env:ProgramData "Microsoft\Windows\Start Menu\Programs"),
(Join-Path $env:USERPROFILE "Desktop"),
(Join-Path $env:PUBLIC "Desktop")
)

foreach ($location in $shortcutLocations) {
if (-not (Test-Path $location)) {
continue
}

foreach ($variant in $script:AppVariants) {
$shortcuts = Get-ChildItem -Path $location -Filter "*$variant*.lnk" -ErrorAction SilentlyContinue
if ($null -eq $shortcuts) {
continue
}

foreach ($shortcut in $shortcuts) {
$foundShortcuts = $true
Write-Status "Found shortcut: $($shortcut.Name)"

if ($PSCmdlet.ShouldProcess($shortcut.FullName, "Remove shortcut")) {
try {
Remove-Item -Path $shortcut.FullName -Force -ErrorAction Stop
$script:CleanupResults.ShortcutsRemoved += $shortcut.FullName
Write-Status "Removed: $($shortcut.Name)" -Type Success
}
catch {
Write-Status "Failed to remove shortcut: $($_.Exception.Message)" -Type Warning
$script:CleanupResults.Errors += "Failed to remove shortcut: $($shortcut.FullName)"
}
}
}
}
}

if (-not $foundShortcuts) {
Write-Status "No shortcuts found" -Type Info
}
}

function Remove-TempFiles {
<#
.SYNOPSIS
Removes temporary files related to WhatPulse.
#>
Write-SectionHeader "Removing Temporary Files"

$foundTemp = $false
$tempDir = $env:TEMP

if (-not (Test-Path $tempDir)) {
Write-Status "Temp directory not found" -Type Info
return
}

# Look for WhatPulse-specific temp files and folders
# Use explicit patterns to avoid matching unrelated files
$patterns = @(
"WhatPulse*",
"WhatPulse-Professional*",
"qtsingleapp-whatpulse*",
"qtsingleapp-whatpu*"
)

foreach ($pattern in $patterns) {
$items = Get-ChildItem -Path $tempDir -Filter $pattern -ErrorAction SilentlyContinue
if ($null -eq $items) {
continue
}

foreach ($item in $items) {
$foundTemp = $true
Write-Status "Found: $($item.Name)"

if ($PSCmdlet.ShouldProcess($item.FullName, "Remove temp file/folder")) {
try {
Remove-Item -Path $item.FullName -Recurse -Force -ErrorAction Stop
Write-Status "Removed: $($item.Name)" -Type Success
}
catch {
# Files in use are expected, don't treat as error
Write-Status "Skipped (in use): $($item.Name)" -Type Info
}
}
}
}

if (-not $foundTemp) {
Write-Status "No temporary files found" -Type Info
}
}

function Write-CleanupSummary {
<#
.SYNOPSIS
Displays a summary of all cleanup actions performed.
#>
Write-Host ""
Write-Host "========================================" -ForegroundColor Magenta
Write-Host " Cleanup Summary" -ForegroundColor Magenta
Write-Host "========================================" -ForegroundColor Magenta
Write-Host ""

if ($WhatIfPreference) {
Write-Status "DRY RUN - No changes were made" -Type Warning
Write-Host ""
}

# Processes
Write-Host "Processes stopped:" -ForegroundColor White
if ($script:CleanupResults.ProcessesStopped.Count -gt 0) {
foreach ($item in $script:CleanupResults.ProcessesStopped) {
Write-Host " [X] $item" -ForegroundColor Green
}
}
else {
Write-Host " (none)" -ForegroundColor Gray
}

# Registry
Write-Host ""
Write-Host "Registry entries removed:" -ForegroundColor White
if ($script:CleanupResults.RegistryKeysRemoved.Count -gt 0) {
foreach ($item in $script:CleanupResults.RegistryKeysRemoved) {
Write-Host " [X] $item" -ForegroundColor Green
}
}
else {
Write-Host " (none)" -ForegroundColor Gray
}

# Tasks
Write-Host ""
Write-Host "Scheduled tasks removed:" -ForegroundColor White
if ($script:CleanupResults.TasksRemoved.Count -gt 0) {
foreach ($item in $script:CleanupResults.TasksRemoved) {
Write-Host " [X] $item" -ForegroundColor Green
}
}
else {
Write-Host " (none)" -ForegroundColor Gray
}

# Directories
Write-Host ""
Write-Host "Directories removed:" -ForegroundColor White
if ($script:CleanupResults.DirectoriesRemoved.Count -gt 0) {
foreach ($item in $script:CleanupResults.DirectoriesRemoved) {
Write-Host " [X] $item" -ForegroundColor Green
}
}
else {
Write-Host " (none)" -ForegroundColor Gray
}

# Shortcuts
Write-Host ""
Write-Host "Shortcuts removed:" -ForegroundColor White
if ($script:CleanupResults.ShortcutsRemoved.Count -gt 0) {
foreach ($item in $script:CleanupResults.ShortcutsRemoved) {
Write-Host " [X] $item" -ForegroundColor Green
}
}
else {
Write-Host " (none)" -ForegroundColor Gray
}

# Errors
if ($script:CleanupResults.Errors.Count -gt 0) {
Write-Host ""
Write-Host "Warnings/Errors:" -ForegroundColor Yellow
foreach ($item in $script:CleanupResults.Errors) {
Write-Host " [!] $item" -ForegroundColor Yellow
}
}

Write-Host ""
Write-Host "========================================" -ForegroundColor Magenta

# Calculate totals
$totalRemoved = $script:CleanupResults.ProcessesStopped.Count +
$script:CleanupResults.RegistryKeysRemoved.Count +
$script:CleanupResults.TasksRemoved.Count +
$script:CleanupResults.DirectoriesRemoved.Count +
$script:CleanupResults.ShortcutsRemoved.Count

if ($totalRemoved -gt 0) {
Write-Status "Cleanup complete: $totalRemoved item(s) processed" -Type Success
}
else {
Write-Status "No WhatPulse components found to remove" -Type Info
}

if ($script:CleanupResults.Errors.Count -gt 0) {
Write-Status "$($script:CleanupResults.Errors.Count) warning(s) occurred" -Type Warning
}

Write-Host ""
}

#endregion

#region Main Execution

function Main {
# Display header
Write-Host ""
Write-Host "========================================" -ForegroundColor Magenta
Write-Host " WhatPulse Complete Cleanup Script" -ForegroundColor Magenta
Write-Host "========================================" -ForegroundColor Magenta
Write-Host ""
Write-Host " Variants: $($script:AppVariants -join ', ')" -ForegroundColor Gray
Write-Host ""

# Check admin rights
if (-not (Test-IsAdmin)) {
Write-Status "Running without Administrator privileges" -Type Warning
Write-Status "Some operations may fail. Consider running as Administrator." -Type Warning
}
else {
Write-Status "Running with Administrator privileges" -Type Success
}

# Show mode
if ($WhatIfPreference) {
Write-Host ""
Write-Status "DRY RUN MODE - No changes will be made" -Type Warning
}

if ($SkipAppData) {
Write-Status "User data will be preserved" -Type Info
}

if (-not [string]::IsNullOrWhiteSpace($InstallDir)) {
Write-Status "Custom install path: $InstallDir" -Type Info
}

# Execute cleanup steps in order
Stop-WhatPulseProcesses
Remove-RegistryEntries
Remove-ScheduledTasks
Remove-AppDataDirectories
Remove-InstallationDirectories
Remove-StartMenuShortcuts
Remove-TempFiles

# Display summary
Write-CleanupSummary
}

# Run main function
Main

#endregion
}
}

function Remove-AppDataDirectories {
<#
.SYNOPSIS
Removes application data directories (user data, database, settings).
#>
Write-SectionHeader "Removing Application Data"

if ($SkipAppData) {
Write-Status " Skipping application data (preserving user data)" -Type Warning
return
}

$appDataPaths = Get-AppDataPaths
$foundAppData = $false

foreach ($appDataPath in $appDataPaths) {
if (Test-Path $appDataPath) {
$foundAppData = $true
Remove-ItemSafely -Path $appDataPath -Description "application data"
}
}

if (-not $foundAppData) {
Write-Status " No application data directories found" -Type Info
}
}

function Remove-InstallationDirectories {
<#
.SYNOPSIS
Removes installation directories.
#>
Write-SectionHeader "Removing Installation Directories"

$installPaths = Get-InstallationPaths -CustomPath $InstallDir
$foundInstall = $false

Write-Status " Checking installation paths..."

foreach ($installPath in $installPaths) {
if (Test-Path $installPath) {
$foundInstall = $true
Remove-ItemSafely -Path $installPath -Description "installation directory"
}
}

if (-not $foundInstall) {
Write-Status " No installation directories found" -Type Info
Write-Status " Checked locations:" -Type Info
foreach ($path in $installPaths) {
Write-Status " - $path" -Type Info
}
}
}

function Remove-StartMenuShortcuts {
<#
.SYNOPSIS
Removes Start Menu folders and shortcuts.
#>
Write-SectionHeader "Removing Start Menu Shortcuts"

$foundShortcuts = $false

# Start Menu program folders
foreach ($variant in $script:AppVariants) {
$startMenuPaths = @(
(Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs\$variant"),
(Join-Path $env:ProgramData "Microsoft\Windows\Start Menu\Programs\$variant")
)

foreach ($menuPath in $startMenuPaths) {
if (Test-Path $menuPath) {
$foundShortcuts = $true
Remove-ItemSafely -Path $menuPath -Description "Start Menu folder"
}
}
}

# Individual shortcut files
$shortcutLocations = @(
(Join-Path $env:APPDATA "Microsoft\Windows\Start Menu\Programs"),
(Join-Path $env:ProgramData "Microsoft\Windows\Start Menu\Programs"),
(Join-Path $env:USERPROFILE "Desktop"),
(Join-Path $env:PUBLIC "Desktop")
)

foreach ($location in $shortcutLocations) {
if (Test-Path $location) {
foreach ($variant in $script:AppVariants) {
Get-ChildItem -Path $location -Filter "*$variant*.lnk" -ErrorAction SilentlyContinue | ForEach-Object {
$foundShortcuts = $true
Write-Status " Found shortcut: $($_.Name)"

if ($PSCmdlet.ShouldProcess($_.FullName, "Remove shortcut")) {
try {
Remove-Item -Path $_.FullName -Force -ErrorAction Stop
$script:CleanupResults.ShortcutsRemoved += $_.FullName
Write-Status " Removed: $($_.Name)" -Type Success
}
catch {
Write-Status " Failed to remove shortcut: $($_.Exception.Message)" -Type Warning
$script:CleanupResults.Errors += "Failed to remove shortcut: $($_.FullName)"
}
}
}
}
}
}

if (-not $foundShortcuts) {
Write-Status " No shortcuts found" -Type Info
}
}

function Remove-TempFiles {
<#
.SYNOPSIS
Removes temporary files related to WhatPulse.
#>
Write-SectionHeader "Removing Temporary Files"

$foundTemp = $false

foreach ($variant in $script:AppVariants) {
$patterns = @("$variant*", $variant.ToLower() + "*")

foreach ($pattern in $patterns) {
$tempPath = Join-Path $env:TEMP $pattern
Get-Item -Path $tempPath -ErrorAction SilentlyContinue | ForEach-Object {
$foundTemp = $true
Write-Status " Found: $($_.Name)"

if ($PSCmdlet.ShouldProcess($_.FullName, "Remove temp file/folder")) {
try {
Remove-Item -Path $_.FullName -Recurse -Force -ErrorAction Stop
Write-Status " Removed: $($_.Name)" -Type Success
}
catch {
Write-Status " Failed to remove: $($_.Exception.Message)" -Type Warning
}
}
}
}
}

if (-not $foundTemp) {
Write-Status " No temporary files found" -Type Info
}
}

function Write-CleanupSummary {
<#
.SYNOPSIS
Displays a summary of all cleanup actions performed.
#>
Write-Host ""
Write-Host "========================================" -ForegroundColor Magenta
Write-Host " Cleanup Summary" -ForegroundColor Magenta
Write-Host "========================================" -ForegroundColor Magenta
Write-Host ""

if ($WhatIfPreference) {
Write-Status "DRY RUN - No changes were made" -Type Warning
Write-Host ""
}

# Processes
Write-Host "Processes stopped:" -ForegroundColor White
if ($script:CleanupResults.ProcessesStopped.Count -gt 0) {
foreach ($item in $script:CleanupResults.ProcessesStopped) {
Write-Host " [X] $item" -ForegroundColor Green
}
}
else {
Write-Host " (none)" -ForegroundColor Gray
}

# Registry
Write-Host ""
Write-Host "Registry entries removed:" -ForegroundColor White
if ($script:CleanupResults.RegistryKeysRemoved.Count -gt 0) {
foreach ($item in $script:CleanupResults.RegistryKeysRemoved) {
Write-Host " [X] $item" -ForegroundColor Green
}
}
else {
Write-Host " (none)" -ForegroundColor Gray
}

# Tasks
Write-Host ""
Write-Host "Scheduled tasks removed:" -ForegroundColor White
if ($script:CleanupResults.TasksRemoved.Count -gt 0) {
foreach ($item in $script:CleanupResults.TasksRemoved) {
Write-Host " [X] $item" -ForegroundColor Green
}
}
else {
Write-Host " (none)" -ForegroundColor Gray
}

# Directories
Write-Host ""
Write-Host "Directories removed:" -ForegroundColor White
if ($script:CleanupResults.DirectoriesRemoved.Count -gt 0) {
foreach ($item in $script:CleanupResults.DirectoriesRemoved) {
Write-Host " [X] $item" -ForegroundColor Green
}
}
else {
Write-Host " (none)" -ForegroundColor Gray
}

# Shortcuts
Write-Host ""
Write-Host "Shortcuts removed:" -ForegroundColor White
if ($script:CleanupResults.ShortcutsRemoved.Count -gt 0) {
foreach ($item in $script:CleanupResults.ShortcutsRemoved) {
Write-Host " [X] $item" -ForegroundColor Green
}
}
else {
Write-Host " (none)" -ForegroundColor Gray
}

# Errors
if ($script:CleanupResults.Errors.Count -gt 0) {
Write-Host ""
Write-Host "Warnings/Errors:" -ForegroundColor Yellow
foreach ($item in $script:CleanupResults.Errors) {
Write-Host " [!] $item" -ForegroundColor Yellow
}
}

Write-Host ""
Write-Host "========================================" -ForegroundColor Magenta

# Calculate totals
$totalRemoved = $script:CleanupResults.ProcessesStopped.Count +
$script:CleanupResults.RegistryKeysRemoved.Count +
$script:CleanupResults.TasksRemoved.Count +
$script:CleanupResults.DirectoriesRemoved.Count +
$script:CleanupResults.ShortcutsRemoved.Count

if ($totalRemoved -gt 0) {
Write-Status "Cleanup complete: $totalRemoved items processed" -Type Success
}
else {
Write-Status "No WhatPulse components found to remove" -Type Info
}

if ($script:CleanupResults.Errors.Count -gt 0) {
Write-Status "$($script:CleanupResults.Errors.Count) warning(s) occurred" -Type Warning
}

Write-Host ""
}

#endregion

#region Main Execution

function Main {
# Display header
Write-Host ""
Write-Host "========================================" -ForegroundColor Magenta
Write-Host " WhatPulse Complete Cleanup Script" -ForegroundColor Magenta
Write-Host "========================================" -ForegroundColor Magenta
Write-Host ""
Write-Host " Variants: $($script:AppVariants -join ', ')" -ForegroundColor Gray
Write-Host ""

# Check admin rights
if (-not (Test-IsAdmin)) {
Write-Status "Running without Administrator privileges" -Type Warning
Write-Status "Some operations may fail. Consider running as Administrator." -Type Warning
}
else {
Write-Status "Running with Administrator privileges" -Type Success
}

# Show mode
if ($WhatIfPreference) {
Write-Host ""
Write-Status "DRY RUN MODE - No changes will be made" -Type Warning
}

if ($SkipAppData) {
Write-Status "User data will be preserved" -Type Info
}

if (-not [string]::IsNullOrWhiteSpace($InstallDir)) {
Write-Status "Custom install path: $InstallDir" -Type Info
}

# Execute cleanup steps in order
Stop-WhatPulseProcesses
Remove-RegistryEntries
Remove-ScheduledTasks
Remove-AppDataDirectories
Remove-InstallationDirectories
Remove-StartMenuShortcuts
Remove-TempFiles

# Display summary
Write-CleanupSummary
}

# Run main function
Main

#endregion