Windows FMA - Windsurf (#46401)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added Windows support for Windsurf as a managed app (version 2.3.15)
with packaged manifest and installer metadata.
  * Added automated silent install and uninstall flows for Windows.

* **Bug Fixes**
* Improved post-install/uninstall process handling to avoid file-locks.
* Enhanced uninstall detection and exit-code handling to better report
success/failure.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/fleetdm/fleet/pull/46401?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Allen Houchins <allenhouchins@mac.com>
This commit is contained in:
Harrison Ravazzolo
2026-05-28 22:22:18 -05:00
committed by GitHub
co-authored by Allen Houchins
parent 2ba95035e1
commit a93ea6974d
5 changed files with 178 additions and 0 deletions
@@ -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
}
@@ -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 -and -not $selected.QuietUninstallString)) {
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
}
@@ -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"]
}
+7
View File
@@ -2423,6 +2423,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",
@@ -0,0 +1,22 @@
{
"versions": [
{
"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.15') < 0);"
},
"installer_url": "https://windsurf-stable.codeiumdata.com/win32-x64/stable/c46c49e94b4d3f41181204d59809d8f1b2c48d68/WindsurfSetup-x64-2.3.15.exe",
"install_script_ref": "232abd06",
"uninstall_script_ref": "bb230cc3",
"sha256": "d30ff9c6a490597aac354746b689335587f6bf8da671b935e8d1be51b3825d15",
"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",
"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"
}
}