From 4d832fba49849cee6a50f46f1cf146c211b8acc0 Mon Sep 17 00:00:00 2001 From: Harrison Ravazzolo <38767391+harrisonravazzolo@users.noreply.github.com> Date: Thu, 28 May 2026 15:29:34 -0700 Subject: [PATCH 1/4] add windsurf fma --- .../winget/scripts/windsurf_install.ps1 | 44 +++++++++ .../winget/scripts/windsurf_uninstall.ps1 | 93 +++++++++++++++++++ .../inputs/winget/windsurf.json | 12 +++ .../outputs/windsurf/windows.json | 22 +++++ 4 files changed, 171 insertions(+) create mode 100644 ee/maintained-apps/inputs/winget/scripts/windsurf_install.ps1 create mode 100644 ee/maintained-apps/inputs/winget/scripts/windsurf_uninstall.ps1 create mode 100644 ee/maintained-apps/inputs/winget/windsurf.json create mode 100644 ee/maintained-apps/outputs/windsurf/windows.json diff --git a/ee/maintained-apps/inputs/winget/scripts/windsurf_install.ps1 b/ee/maintained-apps/inputs/winget/scripts/windsurf_install.ps1 new file mode 100644 index 00000000000..92d26fbb178 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/windsurf_install.ps1 @@ -0,0 +1,44 @@ + +# install switches: +# /VERYSILENT = no UI at all +# /SUPPRESSMSGBOXES = suppress every message box +# /NORESTART = don't reboot (returns 3010 instead, treat as success) +# /ALLUSERS = machine-scope install. Requires elevation; the +# installer self-elevates (manifest: +# ElevationRequirement: elevatesSelf), and Fleet/ +# SYSTEM already runs elevated anyway. +# /MERGETASKS=!runcode = keep default selectable tasks but deselect +# "runcode" (which auto-launches Windsurf after +# install). Inno's "!" prefix means "deselect". + +$exeFilePath = "${env:INSTALLER_PATH}" + +# 0 = success; 3010 = success but reboot required. +$ExpectedExitCodes = @(0, 3010) + +try { + $processOptions = @{ + FilePath = "$exeFilePath" + ArgumentList = "/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART", + "/ALLUSERS", "/MERGETASKS=!runcode" + PassThru = $true + Wait = $true + } + + $process = Start-Process @processOptions + $exitCode = $process.ExitCode + Write-Host "Install exit code: $exitCode" + + # Defensive: even with !runcode, Inno installers occasionally launch the + # app via post-install scripts. Stop Windsurf if it slipped through so + # files aren't locked for later detection/uninstall. + Start-Sleep -Seconds 5 + Stop-Process -Name "Windsurf" -Force -ErrorAction SilentlyContinue + + if ($ExpectedExitCodes -contains $exitCode) { Exit 0 } + Exit $exitCode + +} catch { + Write-Host "Error: $_" + Exit 1 +} diff --git a/ee/maintained-apps/inputs/winget/scripts/windsurf_uninstall.ps1 b/ee/maintained-apps/inputs/winget/scripts/windsurf_uninstall.ps1 new file mode 100644 index 00000000000..88e445cbb53 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/windsurf_uninstall.ps1 @@ -0,0 +1,93 @@ +$softwareNameLike = "Windsurf*" +$publisherLike = "*Codeium*" + +$paths = @( + 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', + 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall', + 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall' +) + +$ExpectedExitCodes = @(0, 3010, 1641) +$exitCode = 0 + +try { + +[array]$uninstallKeys = Get-ChildItem ` + -Path $paths ` + -ErrorAction SilentlyContinue | + ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue } + +$selected = $null +foreach ($key in $uninstallKeys) { + if ($key.DisplayName -and $key.DisplayName -like $softwareNameLike -and $key.Publisher -like $publisherLike) { + $selected = $key + break + } +} + +if (-not $selected -or -not $selected.UninstallString) { + Write-Host "Uninstall entry not found for $softwareNameLike" + Exit 0 +} + +# Stop Windsurf and any helpers running out of the install dir so the +# uninstaller doesn't fail on locked files. +Stop-Process -Name "Windsurf" -Force -ErrorAction SilentlyContinue +if ($selected.InstallLocation -and (Test-Path -LiteralPath $selected.InstallLocation)) { + $loc = $selected.InstallLocation.TrimEnd('\') + Get-Process | Where-Object { $_.Path -and $_.Path -like "$loc\*" } | + ForEach-Object { Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue } +} +Start-Sleep -Seconds 2 + +# Prefer QuietUninstallString -- Inno populates it with /VERYSILENT +# /SUPPRESSMSGBOXES already, so we just add /NORESTART if missing. +$uninstallCommand = if ($selected.QuietUninstallString) { + $selected.QuietUninstallString +} else { + $selected.UninstallString +} + +# Parse the uninstaller exe path. Inno usually quotes it because the install +# path often contains "Program Files". +$exePath = "" +$existingArgs = "" +if ($uninstallCommand -match '^\s*"([^"]+)"\s*(.*)$') { + $exePath = $matches[1] + $existingArgs = $matches[2].Trim() +} elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)\s*(.*)$') { + $exePath = $matches[1] + $existingArgs = $matches[2].Trim() +} else { + Throw "Could not parse uninstall string: $uninstallCommand" +} + + +$argumentList = @() +if ($existingArgs) { $argumentList += ($existingArgs -split '\s+') } +foreach ($s in @("/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART")) { + if ($argumentList -notcontains $s) { $argumentList += $s } +} + +Write-Host "Selected entry DisplayName: $($selected.DisplayName)" +Write-Host "Uninstall command: $exePath" +Write-Host "Uninstall args: $($argumentList -join ' ')" + +$processOptions = @{ + FilePath = $exePath + ArgumentList = $argumentList + PassThru = $true + Wait = $true +} + +$process = Start-Process @processOptions +$exitCode = $process.ExitCode +Write-Host "Uninstall exit code: $exitCode" + +if ($ExpectedExitCodes -contains $exitCode) { Exit 0 } +Exit $exitCode + +} catch { + Write-Host "Error: $_" + Exit 1 +} diff --git a/ee/maintained-apps/inputs/winget/windsurf.json b/ee/maintained-apps/inputs/winget/windsurf.json new file mode 100644 index 00000000000..206b9cbb89b --- /dev/null +++ b/ee/maintained-apps/inputs/winget/windsurf.json @@ -0,0 +1,12 @@ +{ + "name": "Windsurf", + "slug": "windsurf/windows", + "package_identifier": "Codeium.Windsurf", + "unique_identifier": "Windsurf", + "install_script_path": "ee/maintained-apps/inputs/winget/scripts/windsurf_install.ps1", + "uninstall_script_path": "ee/maintained-apps/inputs/winget/scripts/windsurf_uninstall.ps1", + "installer_arch": "x64", + "installer_type": "exe", + "installer_scope": "machine", + "default_categories": ["Developer tools"] +} diff --git a/ee/maintained-apps/outputs/windsurf/windows.json b/ee/maintained-apps/outputs/windsurf/windows.json new file mode 100644 index 00000000000..9ac715245da --- /dev/null +++ b/ee/maintained-apps/outputs/windsurf/windows.json @@ -0,0 +1,22 @@ +{ + "versions": [ + { + "version": "2.3.9", + "queries": { + "exists": "SELECT 1 FROM programs WHERE name = 'Windsurf' AND publisher = 'Codeium';", + "patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'Windsurf' AND publisher = 'Codeium' AND version_compare(version, '2.3.9') < 0);" + }, + "installer_url": "https://windsurf-stable.codeiumdata.com/win32-x64/stable/a5d3f1ff990cabc0e8001cce6642bdb7ad429e73/WindsurfSetup-x64-2.3.9.exe", + "install_script_ref": "232abd06", + "uninstall_script_ref": "52e64cb5", + "sha256": "500a2eae692ba63d0cac90ace3bed8c267a2d9a384c5fe9892ecbca859681967", + "default_categories": [ + "Developer tools" + ] + } + ], + "refs": { + "232abd06": "\n# install switches:\n# /VERYSILENT = no UI at all\n# /SUPPRESSMSGBOXES = suppress every message box\n# /NORESTART = don't reboot (returns 3010 instead, treat as success)\n# /ALLUSERS = machine-scope install. Requires elevation; the\n# installer self-elevates (manifest:\n# ElevationRequirement: elevatesSelf), and Fleet/\n# SYSTEM already runs elevated anyway.\n# /MERGETASKS=!runcode = keep default selectable tasks but deselect\n# \"runcode\" (which auto-launches Windsurf after\n# install). Inno's \"!\" prefix means \"deselect\".\n\n$exeFilePath = \"${env:INSTALLER_PATH}\"\n\n# 0 = success; 3010 = success but reboot required.\n$ExpectedExitCodes = @(0, 3010)\n\ntry {\n $processOptions = @{\n FilePath = \"$exeFilePath\"\n ArgumentList = \"/VERYSILENT\", \"/SUPPRESSMSGBOXES\", \"/NORESTART\",\n \"/ALLUSERS\", \"/MERGETASKS=!runcode\"\n PassThru = $true\n Wait = $true\n }\n\n $process = Start-Process @processOptions\n $exitCode = $process.ExitCode\n Write-Host \"Install exit code: $exitCode\"\n\n # Defensive: even with !runcode, Inno installers occasionally launch the\n # app via post-install scripts. Stop Windsurf if it slipped through so\n # files aren't locked for later detection/uninstall.\n Start-Sleep -Seconds 5\n Stop-Process -Name \"Windsurf\" -Force -ErrorAction SilentlyContinue\n\n if ($ExpectedExitCodes -contains $exitCode) { Exit 0 }\n Exit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n", + "52e64cb5": "$softwareNameLike = \"Windsurf*\"\n$publisherLike = \"*Codeium*\"\n\n$paths = @(\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall'\n)\n\n$ExpectedExitCodes = @(0, 3010, 1641)\n$exitCode = 0\n\ntry {\n\n[array]$uninstallKeys = Get-ChildItem `\n -Path $paths `\n -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue }\n\n$selected = $null\nforeach ($key in $uninstallKeys) {\n if ($key.DisplayName -and $key.DisplayName -like $softwareNameLike -and $key.Publisher -like $publisherLike) {\n $selected = $key\n break\n }\n}\n\nif (-not $selected -or -not $selected.UninstallString) {\n Write-Host \"Uninstall entry not found for $softwareNameLike\"\n Exit 0\n}\n\n# Stop Windsurf and any helpers running out of the install dir so the\n# uninstaller doesn't fail on locked files.\nStop-Process -Name \"Windsurf\" -Force -ErrorAction SilentlyContinue\nif ($selected.InstallLocation -and (Test-Path -LiteralPath $selected.InstallLocation)) {\n $loc = $selected.InstallLocation.TrimEnd('\\')\n Get-Process | Where-Object { $_.Path -and $_.Path -like \"$loc\\*\" } |\n ForEach-Object { Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue }\n}\nStart-Sleep -Seconds 2\n\n# Prefer QuietUninstallString -- Inno populates it with /VERYSILENT\n# /SUPPRESSMSGBOXES already, so we just add /NORESTART if missing.\n$uninstallCommand = if ($selected.QuietUninstallString) {\n $selected.QuietUninstallString\n} else {\n $selected.UninstallString\n}\n\n# Parse the uninstaller exe path. Inno usually quotes it because the install\n# path often contains \"Program Files\".\n$exePath = \"\"\n$existingArgs = \"\"\nif ($uninstallCommand -match '^\\s*\"([^\"]+)\"\\s*(.*)$') {\n $exePath = $matches[1]\n $existingArgs = $matches[2].Trim()\n} elseif ($uninstallCommand -match '(?i)^\\s*(.+?\\.exe)\\s*(.*)$') {\n $exePath = $matches[1]\n $existingArgs = $matches[2].Trim()\n} else {\n Throw \"Could not parse uninstall string: $uninstallCommand\"\n}\n\n\n$argumentList = @()\nif ($existingArgs) { $argumentList += ($existingArgs -split '\\s+') }\nforeach ($s in @(\"/VERYSILENT\", \"/SUPPRESSMSGBOXES\", \"/NORESTART\")) {\n if ($argumentList -notcontains $s) { $argumentList += $s }\n}\n\nWrite-Host \"Selected entry DisplayName: $($selected.DisplayName)\"\nWrite-Host \"Uninstall command: $exePath\"\nWrite-Host \"Uninstall args: $($argumentList -join ' ')\"\n\n$processOptions = @{\n FilePath = $exePath\n ArgumentList = $argumentList\n PassThru = $true\n Wait = $true\n}\n\n$process = Start-Process @processOptions\n$exitCode = $process.ExitCode\nWrite-Host \"Uninstall exit code: $exitCode\"\n\nif ($ExpectedExitCodes -contains $exitCode) { Exit 0 }\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n" + } +} From 21eca2fc403f9c59d2e5726e0a4adc9eaacba82a Mon Sep 17 00:00:00 2001 From: Harrison Ravazzolo <38767391+harrisonravazzolo@users.noreply.github.com> Date: Thu, 28 May 2026 15:38:13 -0700 Subject: [PATCH 2/4] missing json in commit --- ee/maintained-apps/outputs/apps.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ee/maintained-apps/outputs/apps.json b/ee/maintained-apps/outputs/apps.json index c3ee7bbce0d..1ad5de0331d 100644 --- a/ee/maintained-apps/outputs/apps.json +++ b/ee/maintained-apps/outputs/apps.json @@ -2360,6 +2360,13 @@ "unique_identifier": "com.exafunction.windsurf", "description": "Windsurf is an agentic IDE powered by AI Flow paradigm." }, + { + "name": "Windsurf", + "slug": "windsurf/windows", + "platform": "windows", + "unique_identifier": "Windsurf", + "description": "Windsurf is an agentic IDE powered by AI Flow paradigm." + }, { "name": "WinSCP", "slug": "winscp/windows", From 7be371ec28f034e81c7140cf668b6e5f4cb2c350 Mon Sep 17 00:00:00 2001 From: Allen Houchins Date: Thu, 28 May 2026 22:11:36 -0500 Subject: [PATCH 3/4] Update windsurf_uninstall.ps1 --- ee/maintained-apps/inputs/winget/scripts/windsurf_uninstall.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ee/maintained-apps/inputs/winget/scripts/windsurf_uninstall.ps1 b/ee/maintained-apps/inputs/winget/scripts/windsurf_uninstall.ps1 index 88e445cbb53..4b32456864c 100644 --- a/ee/maintained-apps/inputs/winget/scripts/windsurf_uninstall.ps1 +++ b/ee/maintained-apps/inputs/winget/scripts/windsurf_uninstall.ps1 @@ -25,7 +25,7 @@ foreach ($key in $uninstallKeys) { } } -if (-not $selected -or -not $selected.UninstallString) { +if (-not $selected -or (-not $selected.UninstallString -and -not $selected.QuietUninstallString)) { Write-Host "Uninstall entry not found for $softwareNameLike" Exit 0 } From 12f769d9671944b217ab6b303e4c740d8e2719d6 Mon Sep 17 00:00:00 2001 From: Allen Houchins Date: Thu, 28 May 2026 22:15:12 -0500 Subject: [PATCH 4/4] Update windows.json --- ee/maintained-apps/outputs/windsurf/windows.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ee/maintained-apps/outputs/windsurf/windows.json b/ee/maintained-apps/outputs/windsurf/windows.json index 9ac715245da..280b38913eb 100644 --- a/ee/maintained-apps/outputs/windsurf/windows.json +++ b/ee/maintained-apps/outputs/windsurf/windows.json @@ -1,15 +1,15 @@ { "versions": [ { - "version": "2.3.9", + "version": "2.3.15", "queries": { "exists": "SELECT 1 FROM programs WHERE name = 'Windsurf' AND publisher = 'Codeium';", - "patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'Windsurf' AND publisher = 'Codeium' AND version_compare(version, '2.3.9') < 0);" + "patched": "SELECT 1 WHERE NOT EXISTS (SELECT 1 FROM programs WHERE name = 'Windsurf' AND publisher = 'Codeium' AND version_compare(version, '2.3.15') < 0);" }, - "installer_url": "https://windsurf-stable.codeiumdata.com/win32-x64/stable/a5d3f1ff990cabc0e8001cce6642bdb7ad429e73/WindsurfSetup-x64-2.3.9.exe", + "installer_url": "https://windsurf-stable.codeiumdata.com/win32-x64/stable/c46c49e94b4d3f41181204d59809d8f1b2c48d68/WindsurfSetup-x64-2.3.15.exe", "install_script_ref": "232abd06", - "uninstall_script_ref": "52e64cb5", - "sha256": "500a2eae692ba63d0cac90ace3bed8c267a2d9a384c5fe9892ecbca859681967", + "uninstall_script_ref": "bb230cc3", + "sha256": "d30ff9c6a490597aac354746b689335587f6bf8da671b935e8d1be51b3825d15", "default_categories": [ "Developer tools" ] @@ -17,6 +17,6 @@ ], "refs": { "232abd06": "\n# install switches:\n# /VERYSILENT = no UI at all\n# /SUPPRESSMSGBOXES = suppress every message box\n# /NORESTART = don't reboot (returns 3010 instead, treat as success)\n# /ALLUSERS = machine-scope install. Requires elevation; the\n# installer self-elevates (manifest:\n# ElevationRequirement: elevatesSelf), and Fleet/\n# SYSTEM already runs elevated anyway.\n# /MERGETASKS=!runcode = keep default selectable tasks but deselect\n# \"runcode\" (which auto-launches Windsurf after\n# install). Inno's \"!\" prefix means \"deselect\".\n\n$exeFilePath = \"${env:INSTALLER_PATH}\"\n\n# 0 = success; 3010 = success but reboot required.\n$ExpectedExitCodes = @(0, 3010)\n\ntry {\n $processOptions = @{\n FilePath = \"$exeFilePath\"\n ArgumentList = \"/VERYSILENT\", \"/SUPPRESSMSGBOXES\", \"/NORESTART\",\n \"/ALLUSERS\", \"/MERGETASKS=!runcode\"\n PassThru = $true\n Wait = $true\n }\n\n $process = Start-Process @processOptions\n $exitCode = $process.ExitCode\n Write-Host \"Install exit code: $exitCode\"\n\n # Defensive: even with !runcode, Inno installers occasionally launch the\n # app via post-install scripts. Stop Windsurf if it slipped through so\n # files aren't locked for later detection/uninstall.\n Start-Sleep -Seconds 5\n Stop-Process -Name \"Windsurf\" -Force -ErrorAction SilentlyContinue\n\n if ($ExpectedExitCodes -contains $exitCode) { Exit 0 }\n Exit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n", - "52e64cb5": "$softwareNameLike = \"Windsurf*\"\n$publisherLike = \"*Codeium*\"\n\n$paths = @(\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall'\n)\n\n$ExpectedExitCodes = @(0, 3010, 1641)\n$exitCode = 0\n\ntry {\n\n[array]$uninstallKeys = Get-ChildItem `\n -Path $paths `\n -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue }\n\n$selected = $null\nforeach ($key in $uninstallKeys) {\n if ($key.DisplayName -and $key.DisplayName -like $softwareNameLike -and $key.Publisher -like $publisherLike) {\n $selected = $key\n break\n }\n}\n\nif (-not $selected -or -not $selected.UninstallString) {\n Write-Host \"Uninstall entry not found for $softwareNameLike\"\n Exit 0\n}\n\n# Stop Windsurf and any helpers running out of the install dir so the\n# uninstaller doesn't fail on locked files.\nStop-Process -Name \"Windsurf\" -Force -ErrorAction SilentlyContinue\nif ($selected.InstallLocation -and (Test-Path -LiteralPath $selected.InstallLocation)) {\n $loc = $selected.InstallLocation.TrimEnd('\\')\n Get-Process | Where-Object { $_.Path -and $_.Path -like \"$loc\\*\" } |\n ForEach-Object { Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue }\n}\nStart-Sleep -Seconds 2\n\n# Prefer QuietUninstallString -- Inno populates it with /VERYSILENT\n# /SUPPRESSMSGBOXES already, so we just add /NORESTART if missing.\n$uninstallCommand = if ($selected.QuietUninstallString) {\n $selected.QuietUninstallString\n} else {\n $selected.UninstallString\n}\n\n# Parse the uninstaller exe path. Inno usually quotes it because the install\n# path often contains \"Program Files\".\n$exePath = \"\"\n$existingArgs = \"\"\nif ($uninstallCommand -match '^\\s*\"([^\"]+)\"\\s*(.*)$') {\n $exePath = $matches[1]\n $existingArgs = $matches[2].Trim()\n} elseif ($uninstallCommand -match '(?i)^\\s*(.+?\\.exe)\\s*(.*)$') {\n $exePath = $matches[1]\n $existingArgs = $matches[2].Trim()\n} else {\n Throw \"Could not parse uninstall string: $uninstallCommand\"\n}\n\n\n$argumentList = @()\nif ($existingArgs) { $argumentList += ($existingArgs -split '\\s+') }\nforeach ($s in @(\"/VERYSILENT\", \"/SUPPRESSMSGBOXES\", \"/NORESTART\")) {\n if ($argumentList -notcontains $s) { $argumentList += $s }\n}\n\nWrite-Host \"Selected entry DisplayName: $($selected.DisplayName)\"\nWrite-Host \"Uninstall command: $exePath\"\nWrite-Host \"Uninstall args: $($argumentList -join ' ')\"\n\n$processOptions = @{\n FilePath = $exePath\n ArgumentList = $argumentList\n PassThru = $true\n Wait = $true\n}\n\n$process = Start-Process @processOptions\n$exitCode = $process.ExitCode\nWrite-Host \"Uninstall exit code: $exitCode\"\n\nif ($ExpectedExitCodes -contains $exitCode) { Exit 0 }\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n" + "bb230cc3": "$softwareNameLike = \"Windsurf*\"\n$publisherLike = \"*Codeium*\"\n\n$paths = @(\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKCU:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall'\n)\n\n$ExpectedExitCodes = @(0, 3010, 1641)\n$exitCode = 0\n\ntry {\n\n[array]$uninstallKeys = Get-ChildItem `\n -Path $paths `\n -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue }\n\n$selected = $null\nforeach ($key in $uninstallKeys) {\n if ($key.DisplayName -and $key.DisplayName -like $softwareNameLike -and $key.Publisher -like $publisherLike) {\n $selected = $key\n break\n }\n}\n\nif (-not $selected -or (-not $selected.UninstallString -and -not $selected.QuietUninstallString)) {\n Write-Host \"Uninstall entry not found for $softwareNameLike\"\n Exit 0\n}\n\n# Stop Windsurf and any helpers running out of the install dir so the\n# uninstaller doesn't fail on locked files.\nStop-Process -Name \"Windsurf\" -Force -ErrorAction SilentlyContinue\nif ($selected.InstallLocation -and (Test-Path -LiteralPath $selected.InstallLocation)) {\n $loc = $selected.InstallLocation.TrimEnd('\\')\n Get-Process | Where-Object { $_.Path -and $_.Path -like \"$loc\\*\" } |\n ForEach-Object { Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue }\n}\nStart-Sleep -Seconds 2\n\n# Prefer QuietUninstallString -- Inno populates it with /VERYSILENT\n# /SUPPRESSMSGBOXES already, so we just add /NORESTART if missing.\n$uninstallCommand = if ($selected.QuietUninstallString) {\n $selected.QuietUninstallString\n} else {\n $selected.UninstallString\n}\n\n# Parse the uninstaller exe path. Inno usually quotes it because the install\n# path often contains \"Program Files\".\n$exePath = \"\"\n$existingArgs = \"\"\nif ($uninstallCommand -match '^\\s*\"([^\"]+)\"\\s*(.*)$') {\n $exePath = $matches[1]\n $existingArgs = $matches[2].Trim()\n} elseif ($uninstallCommand -match '(?i)^\\s*(.+?\\.exe)\\s*(.*)$') {\n $exePath = $matches[1]\n $existingArgs = $matches[2].Trim()\n} else {\n Throw \"Could not parse uninstall string: $uninstallCommand\"\n}\n\n\n$argumentList = @()\nif ($existingArgs) { $argumentList += ($existingArgs -split '\\s+') }\nforeach ($s in @(\"/VERYSILENT\", \"/SUPPRESSMSGBOXES\", \"/NORESTART\")) {\n if ($argumentList -notcontains $s) { $argumentList += $s }\n}\n\nWrite-Host \"Selected entry DisplayName: $($selected.DisplayName)\"\nWrite-Host \"Uninstall command: $exePath\"\nWrite-Host \"Uninstall args: $($argumentList -join ' ')\"\n\n$processOptions = @{\n FilePath = $exePath\n ArgumentList = $argumentList\n PassThru = $true\n Wait = $true\n}\n\n$process = Start-Process @processOptions\n$exitCode = $process.ExitCode\nWrite-Host \"Uninstall exit code: $exitCode\"\n\nif ($ExpectedExitCodes -contains $exitCode) { Exit 0 }\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n" } }