Skip to content

Add Vivaldi as a Fleet-maintained app - #41552

Merged
allenhouchins merged 13 commits into
mainfrom
add-vivaldi-fma
Jun 18, 2026
Merged

Add Vivaldi as a Fleet-maintained app#41552
allenhouchins merged 13 commits into
mainfrom
add-vivaldi-fma

Conversation

@tux234

@tux234 tux234 commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Related issue: N/A — adds Vivaldi to the Fleet-maintained apps catalog.

Summary

Adds Vivaldi as a Fleet-maintained app for macOS and Windows.

  • macOS (vivaldi/darwin) — Homebrew cask input plus a VivaldiDMGInstaller enricher that redirects Homebrew's unsupported tar.xz download to Vivaldi's direct universal DMG (https://downloads.vivaldi.com/stable/Vivaldi.{version}.universal.dmg), keeping installer_format: dmg so the standard hdiutil-based install/uninstall scripts are generated. unique_identifier is the verified bundle id com.vivaldi.Vivaldi.
  • Windows (vivaldi/windows) — winget input (Vivaldi.Vivaldi) with custom install/uninstall scripts. Installs machine-wide with --vivaldi-silent --do-not-launch-chrome --system-level. Because Fleet runs installers as SYSTEM, the --system-level flag is required — without it the Chromium-based installer lands in the SYSTEM profile and is invisible to the real user. The exists query matches the registry DisplayName Vivaldi and verified Publisher Vivaldi Technologies AS.; the uninstall script looks up the entry under HKLM (with an HKCU fallback) and runs the Chromium uninstaller with --force-uninstall.

Outputs were regenerated via the ingester (go run cmd/maintained-apps/main.go --slug=...) at Vivaldi 8.0.4033.46; script refs are content-addressed, not hand-edited.

Changes

  • ee/maintained-apps/inputs/homebrew/vivaldi.json, ingesters/homebrew/external_refs/vivaldi.go (+ registration in main.go) — macOS input and DMG-URL enricher
  • ee/maintained-apps/inputs/winget/vivaldi.json + scripts/vivaldi_install.ps1 / vivaldi_uninstall.ps1 — Windows input and custom machine-wide install/uninstall
  • ee/maintained-apps/outputs/vivaldi/darwin.json, vivaldi/windows.json, outputs/apps.json — generated catalog entries (with descriptions)
  • frontend/pages/SoftwarePage/components/icons/Vivaldi.tsx + index.ts, website/assets/images/app-icon-vivaldi-60x60@2x.png — UI/website icon

Checklist for submitter

  • Input data is properly validated; untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. (Silent-install flags are documented Vivaldi switches; the uninstall script parses the registry UninstallString defensively.)

Testing

  • Regenerated both outputs via the ingester; verified installer_url, SHA, and non-empty install/uninstall script refs.
  • Verified macOS unique_identifier (com.vivaldi.Vivaldi) and Windows identity (DisplayName Vivaldi, Publisher Vivaldi Technologies AS.) against the cask/winget manifests.
  • apps.json validates as JSON; go build ./cmd/maintained-apps/... and GOOS=windows go build ./cmd/maintained-apps/validate/ pass.
  • QA'd all new/changed functionality manually (via the FMA validator on macOS + Windows hosts).

Summary by CodeRabbit

  • New Features
    • Added support for Vivaldi browser across macOS (Homebrew) and Windows (Winget) platforms with version management capabilities.
    • Included automated installation and uninstallation scripts for both operating systems.
    • Added Vivaldi icon to the application interface.

Adds Vivaldi browser as a Fleet Maintained App for macOS (darwin only).
Windows is not included because Vivaldi stable is absent from the
microsoft/winget-pkgs repository (only the Snapshot edition is in WinGet).

macOS uses the enricher pattern to override Homebrew's tar.xz download
with Vivaldi's direct DMG URL, keeping installer_format as "dmg" so the
standard hdiutil-based install script is generated unchanged.
@tux234
tux234 requested review from a team as code owners March 12, 2026 15:39
@tux234 tux234 self-assigned this Mar 12, 2026
@codecov

codecov Bot commented Mar 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 16.66667% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.18%. Comparing base (2bfc902) to head (559931c).
⚠️ Report is 76 commits behind head on main.

Files with missing lines Patch % Lines
...d-apps/ingesters/homebrew/external_refs/vivaldi.go 0.00% 4 Missing ⚠️
...nd/pages/SoftwarePage/components/icons/Vivaldi.tsx 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #41552      +/-   ##
==========================================
- Coverage   67.19%   67.18%   -0.01%     
==========================================
  Files        3621     3630       +9     
  Lines      229297   229333      +36     
  Branches    11945    11751     -194     
==========================================
+ Hits       154069   154086      +17     
- Misses      61370    61397      +27     
+ Partials    13858    13850       -8     
Flag Coverage Δ
backend 68.83% <0.00%> (+<0.01%) ⬆️
frontend 58.02% <50.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@allenhouchins

Copy link
Copy Markdown
Member

Windows is not included: Vivaldi stable is absent from the microsoft/winget-pkgs repository (only VivaldiTechnologies.Vivaldi.Snapshot is in WinGet), so the WinGet ingester cannot be used

This is not accurate. Ex: https://github.com/microsoft/winget-pkgs/blob/master/manifests/v/Vivaldi/Vivaldi/7.8.3925.81/Vivaldi.Vivaldi.installer.yaml

Moving back to draft. This one should probably be redone.

@allenhouchins
allenhouchins marked this pull request as draft March 16, 2026 14:54
…on ASCII

Add Vivaldi browser as a fleet-maintained app for both macOS (homebrew,
tar.xz format) and Windows (winget, exe installer with custom install
and uninstall scripts). Add tar.xz extraction support to the homebrew
ingester scripts. Add frontend icon and app-icon asset.

Fix non-ASCII characters in apps.json: replace curly apostrophe (U+2019)
with straight apostrophe in Adobe Creative Cloud description, and replace
em-dashes (U+2014) with hyphens in Airtame and Cursor descriptions.
@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/vivaldi/darwin.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/vivaldi/windows.json

=== Install Script (no changes) ===
=== Uninstall // 532c3250 -> dd74a7d3 ===

--- /tmp/old.cCVOTg	2026-03-16 19:40:07.456048918 +0000
+++ /tmp/new.tfRLSo	2026-03-16 19:40:07.457048921 +0000
@@ -1,173 +1,58 @@
-$softwareName = "Vivaldi"
+# Uninstall Vivaldi (user-scoped Chromium-based browser)
 
-# Script to uninstall software as the current logged-in user.
-$userScript = @'
+$displayName = "Vivaldi"
 
-# Define acceptable/expected exit codes
-$ExpectedExitCodes = @(0, 19)
-
-$softwareName = "Vivaldi"
-
-# Using the exact software name here is recommended to avoid
-# uninstalling unintended software.
-$softwareNameLike = "*$softwareName*"
-
-# Some uninstallers require additional flags to run silently.
-# Each uninstaller might use a different argument (usually it's "/S" or "/s")
-$uninstallArgs = "--vivaldi-silent --force-uninstall"
-
-$uninstallCommand = ""
-$exitCode = 0
-
-try {
-
-$userKey = `
- 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
-[array]$uninstallKeys = Get-ChildItem `
-    -Path @($userKey) `
-    -ErrorAction SilentlyContinue |
-        ForEach-Object { Get-ItemProperty $_.PSPath }
-
-$foundUninstaller = $false
-foreach ($key in $uninstallKeys) {
-    # If needed, add -notlike to the comparison to exclude certain similar
-    # software
-    if ($key.DisplayName -like $softwareNameLike) {
-        $foundUninstaller = $true
-        # Get the uninstall command. Some uninstallers do not include
-        # 'QuietUninstallString' and require a flag to run silently.
-        $uninstallCommand = if ($key.QuietUninstallString) {
-            $key.QuietUninstallString
-        } else {
-            $key.UninstallString
-        }
-
-        # The uninstall command may contain command and args, like:
-        # "C:\Program Files\Software\uninstall.exe" --uninstall --silent
-        # Split the command and args
-        $splitArgs = $uninstallCommand.Split('"')
-        if ($splitArgs.Length -gt 1) {
-            if ($splitArgs.Length -eq 3) {
-                $uninstallArgs = "$( $splitArgs[2] ) $uninstallArgs".Trim()
-            } elseif ($splitArgs.Length -gt 3) {
-                Throw `
-                    "Uninstall command contains multiple quoted strings. " +
-                        "Please update the uninstall script.`n" +
-                        "Uninstall command: $uninstallCommand"
-            }
-            $uninstallCommand = $splitArgs[1]
-        }
-        Write-Host "Uninstall command: $uninstallCommand"
-        Write-Host "Uninstall args: $uninstallArgs"
-
-        $processOptions = @{
-            FilePath = $uninstallCommand
-            PassThru = $true
-            Wait = $true
-        }
-        if ($uninstallArgs -ne '') {
-            $processOptions.ArgumentList = "$uninstallArgs"
-        }
-
-        # Start the process and track the exit code
-        $process = Start-Process @processOptions
-        $exitCode = $process.ExitCode
-
-        # Prints the exit code
-        Write-Host "Uninstall exit code: $exitCode"
-        # Exit the loop once the software is found and uninstalled.
-        break
-    }
+$paths = @(
+  'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
+  'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
+  'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+)
+
+$uninstall = $null
+foreach ($p in $paths) {
+  $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
+    $_.DisplayName -and $_.DisplayName -eq $displayName
+  }
+  if ($items) { $uninstall = $items | Select-Object -First 1; break }
 }
 
-if (-not $foundUninstaller) {
-    Write-Host "Uninstaller for '$softwareName' not found."
-    $exitCode = 1
+if (-not $uninstall -or (-not $uninstall.UninstallString -and -not $uninstall.QuietUninstallString)) {
+  Write-Host "Uninstall entry not found for '$displayName'"
+  Exit 1
 }
 
-} catch {
-    Write-Host "Error: $_"
-    $exitCode = 1
-}
+# Kill any running Vivaldi processes before uninstalling
+Stop-Process -Name "vivaldi" -Force -ErrorAction SilentlyContinue
+Start-Sleep -Seconds 2
 
-# Treat acceptable exit codes as success
-if ($ExpectedExitCodes -contains $exitCode) {
-    Exit 0
+$uninstallCommand = if ($uninstall.QuietUninstallString) {
+  $uninstall.QuietUninstallString
 } else {
-    Exit $exitCode
+  $uninstall.UninstallString
 }
-'@
 
-$exitCode = 0
+# Parse quoted executable from any trailing args in the registry string
+$splitArgs = $uninstallCommand.Split('"')
+$exe = $splitArgs[1]
+$existingArgs = if ($splitArgs.Length -eq 3) { $splitArgs[2].Trim() } else { "" }
 
-# Create a script in a public folder so that it can be accessed by all users.
-$uninstallScriptPath = "${env:PUBLIC}/uninstall-$softwareName.ps1"
-$taskName = "fleet-uninstall-$softwareName"
-try {
-    Set-Content -Path $uninstallScriptPath -Value $userScript -Force
+# Chromium-based uninstaller flags
+$uninstallArgs = "$existingArgs --uninstall --force-uninstall".Trim()
 
-    # Task properties. The task will be started by the logged in user
-    $action = New-ScheduledTaskAction -Execute "PowerShell.exe" `
-        -Argument "$uninstallScriptPath"
-    $trigger = New-ScheduledTaskTrigger -AtLogOn
-    $userName = (Get-CimInstance Win32_Process -Filter 'name = "explorer.exe"' | Invoke-CimMethod -MethodName getowner).User
-    $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries
-
-    # Create a task object with the properties defined above
-    $task = New-ScheduledTask -Action $action -Trigger $trigger `
-        -Settings $settings
-
-    # Register the task
-    Register-ScheduledTask "$taskName" -InputObject $task -User "$userName"
-
-    # keep track of the start time to cancel if taking too long to start
-    $startDate = Get-Date
-
-    # Start the task now that it is ready
-    Start-ScheduledTask -TaskName "$taskName" -TaskPath "\"
-
-    # Wait for the task to be running
-    $state = (Get-ScheduledTask -TaskName "$taskName").State
-    Write-Host "ScheduledTask is '$state'"
-
-    while ($state  -ne "Running") {
-        Write-Host "ScheduledTask is '$state'. Waiting to uninstall..."
-
-        $endDate = Get-Date
-        $elapsedTime = New-Timespan -Start $startDate -End $endDate
-        if ($elapsedTime.TotalSeconds -gt 120) {
-            Throw "Timed-out waiting for scheduled task state."
-        }
-
-        Start-Sleep -Seconds 1
-        $state = (Get-ScheduledTask -TaskName "$taskName").State
-    }
-
-    # Wait for the task to be done
-    $state = (Get-ScheduledTask -TaskName "$taskName").State
-    while ($state  -eq "Running") {
-        Write-Host "ScheduledTask is '$state'. Waiting for .exe to complete..."
-
-        $endDate = Get-Date
-        $elapsedTime = New-Timespan -Start $startDate -End $endDate
-        if ($elapsedTime.TotalSeconds -gt 120) {
-            Throw "Timed-out waiting for scheduled task state."
-        }
-
-        Start-Sleep -Seconds 10
-        $state = (Get-ScheduledTask -TaskName "$taskName").State
-    }
+Write-Host "Uninstall command: $exe"
+Write-Host "Uninstall args: $uninstallArgs"
 
-} catch {
-    Write-Host "Error: $_"
-    $exitCode = 1
-} finally {
-    # Remove task
-    Write-Host "Removing ScheduledTask: $taskName."
-    Unregister-ScheduledTask -TaskName "$taskName" -Confirm:$false
+try {
+  $process = Start-Process -FilePath $exe -ArgumentList $uninstallArgs -NoNewWindow -PassThru -Wait
+  $exitCode = $process.ExitCode
+  Write-Host "Uninstall exit code: $exitCode"
 
-    # Remove user script
-    Remove-Item -Path $uninstallScriptPath -Force
+  # Chromium uninstallers return 19 on success
+  if ($exitCode -eq 0 -or $exitCode -eq 19) {
+    Exit 0
+  }
+  Exit $exitCode
+} catch {
+  Write-Host "Error running uninstaller: $_"
+  Exit 1
 }
-
-Exit $exitCode

@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/vivaldi/darwin.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/vivaldi/windows.json

=== Install // ab030635 -> 81b0529a ===

--- /tmp/old.eI86eK	2026-03-16 19:44:15.518152475 +0000
+++ /tmp/new.1r5rLm	2026-03-16 19:44:15.518152475 +0000
@@ -1,78 +1,5 @@
-$exeFilePath = "${env:INSTALLER_PATH}"
-
-$exitCode = 0
-
-try {
-
-# Copy the installer to a public folder so that all can access it
-# users
-$exeFilename = Split-Path $exeFilePath -leaf
-Copy-Item -Path $exeFilePath -Destination "${env:PUBLIC}" -Force
-$exeFilePath = "${env:PUBLIC}\$exeFilename"
-
-# Task properties. The task will be started by the logged in user
-$action = New-ScheduledTaskAction -Execute "$exeFilePath" `
-    -Argument "--vivaldi-silent --do-not-launch-chrome"
-$trigger = New-ScheduledTaskTrigger -AtLogOn
-$userName = (Get-CimInstance Win32_Process -Filter 'name = "explorer.exe"' | Invoke-CimMethod -MethodName getowner).User
-$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries
-
-# Create a task object with the properties defined above
-$task = New-ScheduledTask -Action $action -Trigger $trigger `
-    -Settings $settings
-
-# Register the task
-$taskName = "fleet-install-$exeFilename"
-Register-ScheduledTask "$taskName" -InputObject $task -User "$userName"
-
-# keep track of the start time to cancel if taking too long to start
-$startDate = Get-Date
-
-# Start the task now that it is ready
-Start-ScheduledTask -TaskName "$taskName" -TaskPath "\"
-
-# Wait for the task to be running
-$state = (Get-ScheduledTask -TaskName "$taskName").State
-Write-Host "ScheduledTask is '$state'"
-
-while ($state  -ne "Running") {
-    Write-Host "ScheduledTask is '$state'. Waiting to run .exe..."
-
-    $endDate = Get-Date
-    $elapsedTime = New-Timespan -Start $startDate -End $endDate
-    if ($elapsedTime.TotalSeconds -gt 120) {
-        Throw "Timed-out waiting for scheduled task state."
-    }
-
-    Start-Sleep -Seconds 1
-    $state = (Get-ScheduledTask -TaskName "$taskName").State
-}
-
-# Wait for the task to be done
-$state = (Get-ScheduledTask -TaskName "$taskName").State
-while ($state  -eq "Running") {
-    Write-Host "ScheduledTask is '$state'. Waiting for .exe to complete..."
-
-    $endDate = Get-Date
-    $elapsedTime = New-Timespan -Start $startDate -End $endDate
-    if ($elapsedTime.TotalSeconds -gt 120) {
-        Throw "Timed-out waiting for scheduled task state."
-    }
-
-    Start-Sleep -Seconds 10
-    $state = (Get-ScheduledTask -TaskName "$taskName").State
-}
-
-# Remove task
-Write-Host "Removing ScheduledTask: $taskName."
-Unregister-ScheduledTask -TaskName "$taskName" -Confirm:$false
-
-} catch {
-    Write-Host "Error: $_"
-    $exitCode = 1
-} finally {
-    # Remove installer
-    Remove-Item -Path $exeFilePath -Force
-}
-
-Exit $exitCode
+# Install Vivaldi silently (user-scoped Chromium-based browser)
+$process = Start-Process -FilePath $env:INSTALLER_PATH `
+  -ArgumentList "--vivaldi-silent --do-not-launch-chrome" `
+  -NoNewWindow -PassThru -Wait
+Exit $process.ExitCode

=== Uninstall Script (no changes) ===

@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/vivaldi/darwin.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/vivaldi/windows.json

=== Install // ab030635 -> 81b0529a ===

--- /tmp/old.607ZUq	2026-03-16 19:51:25.663634389 +0000
+++ /tmp/new.rLLKxh	2026-03-16 19:51:25.663634389 +0000
@@ -1,78 +1,5 @@
-$exeFilePath = "${env:INSTALLER_PATH}"
-
-$exitCode = 0
-
-try {
-
-# Copy the installer to a public folder so that all can access it
-# users
-$exeFilename = Split-Path $exeFilePath -leaf
-Copy-Item -Path $exeFilePath -Destination "${env:PUBLIC}" -Force
-$exeFilePath = "${env:PUBLIC}\$exeFilename"
-
-# Task properties. The task will be started by the logged in user
-$action = New-ScheduledTaskAction -Execute "$exeFilePath" `
-    -Argument "--vivaldi-silent --do-not-launch-chrome"
-$trigger = New-ScheduledTaskTrigger -AtLogOn
-$userName = (Get-CimInstance Win32_Process -Filter 'name = "explorer.exe"' | Invoke-CimMethod -MethodName getowner).User
-$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries
-
-# Create a task object with the properties defined above
-$task = New-ScheduledTask -Action $action -Trigger $trigger `
-    -Settings $settings
-
-# Register the task
-$taskName = "fleet-install-$exeFilename"
-Register-ScheduledTask "$taskName" -InputObject $task -User "$userName"
-
-# keep track of the start time to cancel if taking too long to start
-$startDate = Get-Date
-
-# Start the task now that it is ready
-Start-ScheduledTask -TaskName "$taskName" -TaskPath "\"
-
-# Wait for the task to be running
-$state = (Get-ScheduledTask -TaskName "$taskName").State
-Write-Host "ScheduledTask is '$state'"
-
-while ($state  -ne "Running") {
-    Write-Host "ScheduledTask is '$state'. Waiting to run .exe..."
-
-    $endDate = Get-Date
-    $elapsedTime = New-Timespan -Start $startDate -End $endDate
-    if ($elapsedTime.TotalSeconds -gt 120) {
-        Throw "Timed-out waiting for scheduled task state."
-    }
-
-    Start-Sleep -Seconds 1
-    $state = (Get-ScheduledTask -TaskName "$taskName").State
-}
-
-# Wait for the task to be done
-$state = (Get-ScheduledTask -TaskName "$taskName").State
-while ($state  -eq "Running") {
-    Write-Host "ScheduledTask is '$state'. Waiting for .exe to complete..."
-
-    $endDate = Get-Date
-    $elapsedTime = New-Timespan -Start $startDate -End $endDate
-    if ($elapsedTime.TotalSeconds -gt 120) {
-        Throw "Timed-out waiting for scheduled task state."
-    }
-
-    Start-Sleep -Seconds 10
-    $state = (Get-ScheduledTask -TaskName "$taskName").State
-}
-
-# Remove task
-Write-Host "Removing ScheduledTask: $taskName."
-Unregister-ScheduledTask -TaskName "$taskName" -Confirm:$false
-
-} catch {
-    Write-Host "Error: $_"
-    $exitCode = 1
-} finally {
-    # Remove installer
-    Remove-Item -Path $exeFilePath -Force
-}
-
-Exit $exitCode
+# Install Vivaldi silently (user-scoped Chromium-based browser)
+$process = Start-Process -FilePath $env:INSTALLER_PATH `
+  -ArgumentList "--vivaldi-silent --do-not-launch-chrome" `
+  -NoNewWindow -PassThru -Wait
+Exit $process.ExitCode

=== Uninstall Script (no changes) ===

@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/vivaldi/darwin.json

=== Install Script (no changes) ===
=== Uninstall Script (no changes) ===

ee/maintained-apps/outputs/vivaldi/windows.json

=== Install // ab030635 -> 81b0529a ===

--- /tmp/old.NIHOXv	2026-03-17 20:40:37.216027936 +0000
+++ /tmp/new.kU1rib	2026-03-17 20:40:37.216027936 +0000
@@ -1,78 +1,5 @@
-$exeFilePath = "${env:INSTALLER_PATH}"
-
-$exitCode = 0
-
-try {
-
-# Copy the installer to a public folder so that all can access it
-# users
-$exeFilename = Split-Path $exeFilePath -leaf
-Copy-Item -Path $exeFilePath -Destination "${env:PUBLIC}" -Force
-$exeFilePath = "${env:PUBLIC}\$exeFilename"
-
-# Task properties. The task will be started by the logged in user
-$action = New-ScheduledTaskAction -Execute "$exeFilePath" `
-    -Argument "--vivaldi-silent --do-not-launch-chrome"
-$trigger = New-ScheduledTaskTrigger -AtLogOn
-$userName = (Get-CimInstance Win32_Process -Filter 'name = "explorer.exe"' | Invoke-CimMethod -MethodName getowner).User
-$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries
-
-# Create a task object with the properties defined above
-$task = New-ScheduledTask -Action $action -Trigger $trigger `
-    -Settings $settings
-
-# Register the task
-$taskName = "fleet-install-$exeFilename"
-Register-ScheduledTask "$taskName" -InputObject $task -User "$userName"
-
-# keep track of the start time to cancel if taking too long to start
-$startDate = Get-Date
-
-# Start the task now that it is ready
-Start-ScheduledTask -TaskName "$taskName" -TaskPath "\"
-
-# Wait for the task to be running
-$state = (Get-ScheduledTask -TaskName "$taskName").State
-Write-Host "ScheduledTask is '$state'"
-
-while ($state  -ne "Running") {
-    Write-Host "ScheduledTask is '$state'. Waiting to run .exe..."
-
-    $endDate = Get-Date
-    $elapsedTime = New-Timespan -Start $startDate -End $endDate
-    if ($elapsedTime.TotalSeconds -gt 120) {
-        Throw "Timed-out waiting for scheduled task state."
-    }
-
-    Start-Sleep -Seconds 1
-    $state = (Get-ScheduledTask -TaskName "$taskName").State
-}
-
-# Wait for the task to be done
-$state = (Get-ScheduledTask -TaskName "$taskName").State
-while ($state  -eq "Running") {
-    Write-Host "ScheduledTask is '$state'. Waiting for .exe to complete..."
-
-    $endDate = Get-Date
-    $elapsedTime = New-Timespan -Start $startDate -End $endDate
-    if ($elapsedTime.TotalSeconds -gt 120) {
-        Throw "Timed-out waiting for scheduled task state."
-    }
-
-    Start-Sleep -Seconds 10
-    $state = (Get-ScheduledTask -TaskName "$taskName").State
-}
-
-# Remove task
-Write-Host "Removing ScheduledTask: $taskName."
-Unregister-ScheduledTask -TaskName "$taskName" -Confirm:$false
-
-} catch {
-    Write-Host "Error: $_"
-    $exitCode = 1
-} finally {
-    # Remove installer
-    Remove-Item -Path $exeFilePath -Force
-}
-
-Exit $exitCode
+# Install Vivaldi silently (user-scoped Chromium-based browser)
+$process = Start-Process -FilePath $env:INSTALLER_PATH `
+  -ArgumentList "--vivaldi-silent --do-not-launch-chrome" `
+  -NoNewWindow -PassThru -Wait
+Exit $process.ExitCode

=== Uninstall Script (no changes) ===

tux234 added 3 commits June 11, 2026 16:01
# Conflicts:
#	ee/maintained-apps/outputs/apps.json
#	frontend/pages/SoftwarePage/components/icons/index.ts
- Rebase onto current main and regenerate darwin/windows outputs via the
  ingester at version 8.0.4033.46 (refs content-addressed, not hand-edited).
- Windows: add --system-level to the install script and set installer_scope
  to machine. Fleet installs as SYSTEM, so without --system-level the
  Chromium-based installer landed in the SYSTEM profile and was invisible to
  the real user. The exists query (HKLM) and uninstall script already assumed
  a machine install. Flag verified against Chrome's FMA script and Vivaldi docs.
- Fill catalog descriptions for both platforms.
…ing parsing

- Reorder the Windows uninstall registry search to HKLM-first (machine-wide
  --system-level install registers under HKLM), with HKCU as a stale-entry
  fallback, and require Publisher 'Vivaldi Technologies AS.' in addition to
  DisplayName to avoid matching the wrong entry.
- Replace the brittle UninstallString .Split('"') parsing with the canonical
  three-case defensive regex (quoted / unquoted-with-spaces / bare token), so
  unquoted registry commands no longer break Start-Process.
- Regenerate vivaldi/windows.json (content-addressed uninstall ref).
@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/vivaldi/darwin.json

=== Install // d8e37a25 -> 6a60dca7 ===

--- /tmp/old.CGH0fk	2026-06-15 14:26:56.346038174 +0000
+++ /tmp/new.KKs3t2	2026-06-15 14:26:56.347038188 +0000
@@ -1,8 +1,8 @@
-#!/bin/sh
+#!/bin/bash
 
 # variables
 APPDIR="/Applications/"
-TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)")
+TMPDIR=$(dirname "$(realpath "$INSTALLER_PATH")")
 # functions
 
 quit_and_track_application() {
@@ -11,14 +11,16 @@
   local timeout_duration=10
 
   # check if the application is running
-  if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then
+  local app_running
+  app_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null)
+  if [[ "$app_running" != "true" ]]; then
     eval "export $var_name=0"
     return
   fi
 
   local console_user
   console_user=$(stat -f "%Su" /dev/console)
-  if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then
+  if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
     echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'."
     eval "export $var_name=0"
     return
@@ -63,15 +65,28 @@
 
   local console_user
   console_user=$(stat -f "%Su" /dev/console)
-  if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then
+  if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
     echo "Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'."
     return
   fi
 
   echo "Relaunching application '$bundle_id'..."
 
-  # Try to launch the application
-  if osascript -e "tell application id \"$bundle_id\" to activate" >/dev/null 2>&1; then
+  # Launch the app in the logged-in user's GUI session. Apps launched by root
+  # won't register with the user's Dock/GUI, so run 'open' as the console user.
+  # Use 'launchctl asuser' to bootstrap into the console user's Mach namespace
+  # and GUI session — 'sudo -u' alone doesn't do this, which can cause
+  # LSOpenURLsWithRole() failures even when 'open' exits 0.
+  local open_status=0
+  if [[ $EUID -eq 0 ]]; then
+    local console_uid
+    console_uid=$(id -u "$console_user")
+    /bin/launchctl asuser "$console_uid" sudo -u "$console_user" open -b "$bundle_id" >/dev/null 2>&1 || open_status=$?
+  else
+    open -b "$bundle_id" >/dev/null 2>&1 || open_status=$?
+  fi
+
+  if [[ $open_status -eq 0 ]]; then
     echo "Application '$bundle_id' relaunched successfully."
   else
     echo "Failed to relaunch application '$bundle_id'."
@@ -81,9 +96,9 @@
 
 # extract contents
 MOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)
-hdiutil attach -plist -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$INSTALLER_PATH"
+yes | hdiutil attach -plist -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$INSTALLER_PATH" || exit 1
 sudo cp -R "$MOUNT_POINT"/* "$TMPDIR"
-hdiutil detach "$MOUNT_POINT"
+hdiutil detach "$MOUNT_POINT" || true
 # copy to the applications folder
 quit_and_track_application 'com.vivaldi.Vivaldi'
 if [ -d "$APPDIR/Vivaldi.app" ]; then

=== Uninstall // 83339974 -> 7c1876ca ===

--- /tmp/old.kHW71h	2026-06-15 14:26:56.368038468 +0000
+++ /tmp/new.i0rzpI	2026-06-15 14:26:56.368038468 +0000
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/bash
 
 # variables
 APPDIR="/Applications/"
@@ -10,13 +10,15 @@
   local timeout_duration=10
 
   # check if the application is running
-  if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then
+  local app_running
+  app_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null)
+  if [[ "$app_running" != "true" ]]; then
     return
   fi
 
   local console_user
   console_user=$(stat -f "%Su" /dev/console)
-  if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then
+  if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
     echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'."
     return
   fi
@@ -55,6 +57,31 @@
   fi
 
   local trash="/Users/$logged_in_user/.Trash"
+
+  # If the target contains glob characters, expand it and move each match.
+  if [[ "$target_file" == *[*?[]* ]]; then
+    local file file_name
+    local matched=false
+    local i=0
+    # compgen -G expands the (quoted) pattern itself, so paths containing
+    # spaces glob correctly; reading line by line keeps each match intact.
+    while IFS= read -r file; do
+      [[ -n "$file" ]] || continue
+      [[ -e "$file" || -L "$file" ]] || continue
+      matched=true
+      i=$((i + 1))
+      file_name="$(basename "$file")"
+      echo "removing $file."
+      # The per-match counter keeps matches that share a basename from
+      # overwriting each other in the trash.
+      mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}"
+    done < <(compgen -G "$target_file" 2>/dev/null)
+    if [[ "$matched" == false ]]; then
+      echo "$target_file doesn't exist."
+    fi
+    return
+  fi
+
   local file_name="$(basename "${target_file}")"
 
   if [[ -e "$target_file" ]]; then

ee/maintained-apps/outputs/vivaldi/windows.json

=== Install Script (no changes) ===
=== Uninstall // 741609fc -> 7b14fec4 ===

--- /tmp/old.CdQeJi	2026-06-15 14:26:56.405038963 +0000
+++ /tmp/new.3yiiI0	2026-06-15 14:26:56.405038963 +0000
@@ -3,17 +3,20 @@
 # fallback, then runs the Chromium uninstaller with --force-uninstall.
 
 $displayName = "Vivaldi"
+$publisher = "Vivaldi Technologies AS."
 
+# Install is machine-wide (--system-level), which registers under HKLM, so look
+# there first. HKCU is only a fallback for a stale user-level install.
 $paths = @(
-  'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
   'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
-  'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+  'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall',
+  'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
 )
 
 $uninstall = $null
 foreach ($p in $paths) {
   $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
-    $_.DisplayName -and $_.DisplayName -eq $displayName
+    $_.DisplayName -eq $displayName -and $_.Publisher -eq $publisher
   }
   if ($items) { $uninstall = $items | Select-Object -First 1; break }
 }
@@ -33,10 +36,21 @@
   $uninstall.UninstallString
 }
 
-# Parse quoted executable from any trailing args in the registry string
-$splitArgs = $uninstallCommand.Split('"')
-$exe = $splitArgs[1]
-$existingArgs = if ($splitArgs.Length -eq 3) { $splitArgs[2].Trim() } else { "" }
+# Parse the executable + trailing args, handling the three registry shapes:
+# quoted, unquoted-with-spaces (capture through .exe), and a bare token.
+if ($uninstallCommand -match '^\s*"([^"]+)"\s*(.*)$') {
+  $exe = $Matches[1]
+  $existingArgs = $Matches[2].Trim()
+} elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
+  $exe = $Matches[1]
+  $existingArgs = $Matches[2].Trim()
+} elseif ($uninstallCommand -match '^\s*(\S+)\s*(.*)$') {
+  $exe = $Matches[1]
+  $existingArgs = $Matches[2].Trim()
+} else {
+  Write-Host "Unable to parse uninstall command: $uninstallCommand"
+  Exit 1
+}
 
 # Chromium-based uninstaller flags
 $uninstallArgs = "$existingArgs --uninstall --force-uninstall".Trim()

@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/vivaldi/darwin.json

=== Install // d8e37a25 -> 6a60dca7 ===

--- /tmp/old.g8QpLm	2026-06-15 16:06:38.101272972 +0000
+++ /tmp/new.Qh1UXy	2026-06-15 16:06:38.101272972 +0000
@@ -1,8 +1,8 @@
-#!/bin/sh
+#!/bin/bash
 
 # variables
 APPDIR="/Applications/"
-TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)")
+TMPDIR=$(dirname "$(realpath "$INSTALLER_PATH")")
 # functions
 
 quit_and_track_application() {
@@ -11,14 +11,16 @@
   local timeout_duration=10
 
   # check if the application is running
-  if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then
+  local app_running
+  app_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null)
+  if [[ "$app_running" != "true" ]]; then
     eval "export $var_name=0"
     return
   fi
 
   local console_user
   console_user=$(stat -f "%Su" /dev/console)
-  if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then
+  if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
     echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'."
     eval "export $var_name=0"
     return
@@ -63,15 +65,28 @@
 
   local console_user
   console_user=$(stat -f "%Su" /dev/console)
-  if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then
+  if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
     echo "Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'."
     return
   fi
 
   echo "Relaunching application '$bundle_id'..."
 
-  # Try to launch the application
-  if osascript -e "tell application id \"$bundle_id\" to activate" >/dev/null 2>&1; then
+  # Launch the app in the logged-in user's GUI session. Apps launched by root
+  # won't register with the user's Dock/GUI, so run 'open' as the console user.
+  # Use 'launchctl asuser' to bootstrap into the console user's Mach namespace
+  # and GUI session — 'sudo -u' alone doesn't do this, which can cause
+  # LSOpenURLsWithRole() failures even when 'open' exits 0.
+  local open_status=0
+  if [[ $EUID -eq 0 ]]; then
+    local console_uid
+    console_uid=$(id -u "$console_user")
+    /bin/launchctl asuser "$console_uid" sudo -u "$console_user" open -b "$bundle_id" >/dev/null 2>&1 || open_status=$?
+  else
+    open -b "$bundle_id" >/dev/null 2>&1 || open_status=$?
+  fi
+
+  if [[ $open_status -eq 0 ]]; then
     echo "Application '$bundle_id' relaunched successfully."
   else
     echo "Failed to relaunch application '$bundle_id'."
@@ -81,9 +96,9 @@
 
 # extract contents
 MOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)
-hdiutil attach -plist -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$INSTALLER_PATH"
+yes | hdiutil attach -plist -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$INSTALLER_PATH" || exit 1
 sudo cp -R "$MOUNT_POINT"/* "$TMPDIR"
-hdiutil detach "$MOUNT_POINT"
+hdiutil detach "$MOUNT_POINT" || true
 # copy to the applications folder
 quit_and_track_application 'com.vivaldi.Vivaldi'
 if [ -d "$APPDIR/Vivaldi.app" ]; then

=== Uninstall // 83339974 -> 7c1876ca ===

--- /tmp/old.mh1ELD	2026-06-15 16:06:38.128272994 +0000
+++ /tmp/new.ut6gZC	2026-06-15 16:06:38.129272995 +0000
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/bash
 
 # variables
 APPDIR="/Applications/"
@@ -10,13 +10,15 @@
   local timeout_duration=10
 
   # check if the application is running
-  if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then
+  local app_running
+  app_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null)
+  if [[ "$app_running" != "true" ]]; then
     return
   fi
 
   local console_user
   console_user=$(stat -f "%Su" /dev/console)
-  if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then
+  if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
     echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'."
     return
   fi
@@ -55,6 +57,31 @@
   fi
 
   local trash="/Users/$logged_in_user/.Trash"
+
+  # If the target contains glob characters, expand it and move each match.
+  if [[ "$target_file" == *[*?[]* ]]; then
+    local file file_name
+    local matched=false
+    local i=0
+    # compgen -G expands the (quoted) pattern itself, so paths containing
+    # spaces glob correctly; reading line by line keeps each match intact.
+    while IFS= read -r file; do
+      [[ -n "$file" ]] || continue
+      [[ -e "$file" || -L "$file" ]] || continue
+      matched=true
+      i=$((i + 1))
+      file_name="$(basename "$file")"
+      echo "removing $file."
+      # The per-match counter keeps matches that share a basename from
+      # overwriting each other in the trash.
+      mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}"
+    done < <(compgen -G "$target_file" 2>/dev/null)
+    if [[ "$matched" == false ]]; then
+      echo "$target_file doesn't exist."
+    fi
+    return
+  fi
+
   local file_name="$(basename "${target_file}")"
 
   if [[ -e "$target_file" ]]; then

ee/maintained-apps/outputs/vivaldi/windows.json

=== Install Script (no changes) ===
=== Uninstall // 741609fc -> 7b14fec4 ===

--- /tmp/old.nj7Cfv	2026-06-15 16:06:38.176273034 +0000
+++ /tmp/new.mKBrwn	2026-06-15 16:06:38.176273034 +0000
@@ -3,17 +3,20 @@
 # fallback, then runs the Chromium uninstaller with --force-uninstall.
 
 $displayName = "Vivaldi"
+$publisher = "Vivaldi Technologies AS."
 
+# Install is machine-wide (--system-level), which registers under HKLM, so look
+# there first. HKCU is only a fallback for a stale user-level install.
 $paths = @(
-  'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
   'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
-  'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+  'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall',
+  'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
 )
 
 $uninstall = $null
 foreach ($p in $paths) {
   $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
-    $_.DisplayName -and $_.DisplayName -eq $displayName
+    $_.DisplayName -eq $displayName -and $_.Publisher -eq $publisher
   }
   if ($items) { $uninstall = $items | Select-Object -First 1; break }
 }
@@ -33,10 +36,21 @@
   $uninstall.UninstallString
 }
 
-# Parse quoted executable from any trailing args in the registry string
-$splitArgs = $uninstallCommand.Split('"')
-$exe = $splitArgs[1]
-$existingArgs = if ($splitArgs.Length -eq 3) { $splitArgs[2].Trim() } else { "" }
+# Parse the executable + trailing args, handling the three registry shapes:
+# quoted, unquoted-with-spaces (capture through .exe), and a bare token.
+if ($uninstallCommand -match '^\s*"([^"]+)"\s*(.*)$') {
+  $exe = $Matches[1]
+  $existingArgs = $Matches[2].Trim()
+} elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
+  $exe = $Matches[1]
+  $existingArgs = $Matches[2].Trim()
+} elseif ($uninstallCommand -match '^\s*(\S+)\s*(.*)$') {
+  $exe = $Matches[1]
+  $existingArgs = $Matches[2].Trim()
+} else {
+  Write-Host "Unable to parse uninstall command: $uninstallCommand"
+  Exit 1
+}
 
 # Chromium-based uninstaller flags
 $uninstallArgs = "$existingArgs --uninstall --force-uninstall".Trim()

@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/vivaldi/darwin.json

=== Install // d8e37a25 -> 6a60dca7 ===

--- /tmp/old.ABWLq3	2026-06-16 13:52:27.946074725 +0000
+++ /tmp/new.8u79TK	2026-06-16 13:52:27.947074718 +0000
@@ -1,8 +1,8 @@
-#!/bin/sh
+#!/bin/bash
 
 # variables
 APPDIR="/Applications/"
-TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)")
+TMPDIR=$(dirname "$(realpath "$INSTALLER_PATH")")
 # functions
 
 quit_and_track_application() {
@@ -11,14 +11,16 @@
   local timeout_duration=10
 
   # check if the application is running
-  if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then
+  local app_running
+  app_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null)
+  if [[ "$app_running" != "true" ]]; then
     eval "export $var_name=0"
     return
   fi
 
   local console_user
   console_user=$(stat -f "%Su" /dev/console)
-  if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then
+  if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
     echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'."
     eval "export $var_name=0"
     return
@@ -63,15 +65,28 @@
 
   local console_user
   console_user=$(stat -f "%Su" /dev/console)
-  if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then
+  if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
     echo "Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'."
     return
   fi
 
   echo "Relaunching application '$bundle_id'..."
 
-  # Try to launch the application
-  if osascript -e "tell application id \"$bundle_id\" to activate" >/dev/null 2>&1; then
+  # Launch the app in the logged-in user's GUI session. Apps launched by root
+  # won't register with the user's Dock/GUI, so run 'open' as the console user.
+  # Use 'launchctl asuser' to bootstrap into the console user's Mach namespace
+  # and GUI session — 'sudo -u' alone doesn't do this, which can cause
+  # LSOpenURLsWithRole() failures even when 'open' exits 0.
+  local open_status=0
+  if [[ $EUID -eq 0 ]]; then
+    local console_uid
+    console_uid=$(id -u "$console_user")
+    /bin/launchctl asuser "$console_uid" sudo -u "$console_user" open -b "$bundle_id" >/dev/null 2>&1 || open_status=$?
+  else
+    open -b "$bundle_id" >/dev/null 2>&1 || open_status=$?
+  fi
+
+  if [[ $open_status -eq 0 ]]; then
     echo "Application '$bundle_id' relaunched successfully."
   else
     echo "Failed to relaunch application '$bundle_id'."
@@ -81,9 +96,9 @@
 
 # extract contents
 MOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)
-hdiutil attach -plist -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$INSTALLER_PATH"
+yes | hdiutil attach -plist -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$INSTALLER_PATH" || exit 1
 sudo cp -R "$MOUNT_POINT"/* "$TMPDIR"
-hdiutil detach "$MOUNT_POINT"
+hdiutil detach "$MOUNT_POINT" || true
 # copy to the applications folder
 quit_and_track_application 'com.vivaldi.Vivaldi'
 if [ -d "$APPDIR/Vivaldi.app" ]; then

=== Uninstall // 83339974 -> 7c1876ca ===

--- /tmp/old.G7Tdiq	2026-06-16 13:52:27.967074564 +0000
+++ /tmp/new.2oiIcz	2026-06-16 13:52:27.968074556 +0000
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/bash
 
 # variables
 APPDIR="/Applications/"
@@ -10,13 +10,15 @@
   local timeout_duration=10
 
   # check if the application is running
-  if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then
+  local app_running
+  app_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null)
+  if [[ "$app_running" != "true" ]]; then
     return
   fi
 
   local console_user
   console_user=$(stat -f "%Su" /dev/console)
-  if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then
+  if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
     echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'."
     return
   fi
@@ -55,6 +57,31 @@
   fi
 
   local trash="/Users/$logged_in_user/.Trash"
+
+  # If the target contains glob characters, expand it and move each match.
+  if [[ "$target_file" == *[*?[]* ]]; then
+    local file file_name
+    local matched=false
+    local i=0
+    # compgen -G expands the (quoted) pattern itself, so paths containing
+    # spaces glob correctly; reading line by line keeps each match intact.
+    while IFS= read -r file; do
+      [[ -n "$file" ]] || continue
+      [[ -e "$file" || -L "$file" ]] || continue
+      matched=true
+      i=$((i + 1))
+      file_name="$(basename "$file")"
+      echo "removing $file."
+      # The per-match counter keeps matches that share a basename from
+      # overwriting each other in the trash.
+      mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}"
+    done < <(compgen -G "$target_file" 2>/dev/null)
+    if [[ "$matched" == false ]]; then
+      echo "$target_file doesn't exist."
+    fi
+    return
+  fi
+
   local file_name="$(basename "${target_file}")"
 
   if [[ -e "$target_file" ]]; then

ee/maintained-apps/outputs/vivaldi/windows.json

=== Install Script (no changes) ===
=== Uninstall // 741609fc -> 7b14fec4 ===

--- /tmp/old.kpUkWy	2026-06-16 13:52:28.013074210 +0000
+++ /tmp/new.dtbWq7	2026-06-16 13:52:28.014074202 +0000
@@ -3,17 +3,20 @@
 # fallback, then runs the Chromium uninstaller with --force-uninstall.
 
 $displayName = "Vivaldi"
+$publisher = "Vivaldi Technologies AS."
 
+# Install is machine-wide (--system-level), which registers under HKLM, so look
+# there first. HKCU is only a fallback for a stale user-level install.
 $paths = @(
-  'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
   'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
-  'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+  'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall',
+  'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
 )
 
 $uninstall = $null
 foreach ($p in $paths) {
   $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
-    $_.DisplayName -and $_.DisplayName -eq $displayName
+    $_.DisplayName -eq $displayName -and $_.Publisher -eq $publisher
   }
   if ($items) { $uninstall = $items | Select-Object -First 1; break }
 }
@@ -33,10 +36,21 @@
   $uninstall.UninstallString
 }
 
-# Parse quoted executable from any trailing args in the registry string
-$splitArgs = $uninstallCommand.Split('"')
-$exe = $splitArgs[1]
-$existingArgs = if ($splitArgs.Length -eq 3) { $splitArgs[2].Trim() } else { "" }
+# Parse the executable + trailing args, handling the three registry shapes:
+# quoted, unquoted-with-spaces (capture through .exe), and a bare token.
+if ($uninstallCommand -match '^\s*"([^"]+)"\s*(.*)$') {
+  $exe = $Matches[1]
+  $existingArgs = $Matches[2].Trim()
+} elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
+  $exe = $Matches[1]
+  $existingArgs = $Matches[2].Trim()
+} elseif ($uninstallCommand -match '^\s*(\S+)\s*(.*)$') {
+  $exe = $Matches[1]
+  $existingArgs = $Matches[2].Trim()
+} else {
+  Write-Host "Unable to parse uninstall command: $uninstallCommand"
+  Exit 1
+}
 
 # Chromium-based uninstaller flags
 $uninstallArgs = "$existingArgs --uninstall --force-uninstall".Trim()

@allenhouchins allenhouchins changed the title Add Vivaldi as a fleet-maintained app Add Vivaldi as a Fleet-maintained app Jun 16, 2026
@allenhouchins
allenhouchins marked this pull request as ready for review June 16, 2026 14:11
Copilot AI review requested due to automatic review settings June 16, 2026 14:11
fleet-release
fleet-release previously approved these changes Jun 16, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Vivaldi to Fleet’s Fleet-maintained apps (FMA) catalog for macOS (Homebrew-based ingestion with an external ref enricher) and Windows (winget-based ingestion with custom PowerShell install/uninstall), plus a frontend icon mapping so Vivaldi displays with the correct logo in the Software UI.

Changes:

  • Add Vivaldi Homebrew + winget inputs and generated outputs for vivaldi/darwin and vivaldi/windows.
  • Introduce a Homebrew external ref enricher to swap Homebrew’s unsupported tar.xz URL for Vivaldi’s direct universal DMG.
  • Add a Vivaldi icon component and wire it into the software-name-to-icon map.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
frontend/pages/SoftwarePage/components/icons/Vivaldi.tsx Adds the Vivaldi icon component.
frontend/pages/SoftwarePage/components/icons/index.ts Registers the Vivaldi icon in the icon map.
ee/maintained-apps/outputs/vivaldi/windows.json Generated Windows Vivaldi catalog entry + embedded script refs.
ee/maintained-apps/outputs/vivaldi/darwin.json Generated macOS Vivaldi catalog entry + embedded script refs.
ee/maintained-apps/outputs/apps.json Adds Vivaldi entries to the generated app catalog index.
ee/maintained-apps/inputs/winget/vivaldi.json Defines the winget input manifest for Vivaldi Windows.
ee/maintained-apps/inputs/winget/scripts/vivaldi_install.ps1 Custom machine-wide silent install script for Windows.
ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1 Custom uninstall script for Windows (registry lookup + forced uninstall).
ee/maintained-apps/inputs/homebrew/vivaldi.json Defines the Homebrew input manifest for Vivaldi macOS.
ee/maintained-apps/ingesters/homebrew/external_refs/vivaldi.go Adds the DMG URL override enricher for Vivaldi macOS.
ee/maintained-apps/ingesters/homebrew/external_refs/main.go Registers the Vivaldi external ref enricher.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread ee/maintained-apps/inputs/winget/scripts/vivaldi_uninstall.ps1
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@github-actions

Copy link
Copy Markdown
Contributor

Script Diff Results

ee/maintained-apps/outputs/vivaldi/darwin.json

=== Install // d8e37a25 -> 6a60dca7 ===

--- /tmp/old.ubOCeZ	2026-06-16 15:17:52.159606724 +0000
+++ /tmp/new.rsrFOZ	2026-06-16 15:17:52.159606724 +0000
@@ -1,8 +1,8 @@
-#!/bin/sh
+#!/bin/bash
 
 # variables
 APPDIR="/Applications/"
-TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)")
+TMPDIR=$(dirname "$(realpath "$INSTALLER_PATH")")
 # functions
 
 quit_and_track_application() {
@@ -11,14 +11,16 @@
   local timeout_duration=10
 
   # check if the application is running
-  if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then
+  local app_running
+  app_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null)
+  if [[ "$app_running" != "true" ]]; then
     eval "export $var_name=0"
     return
   fi
 
   local console_user
   console_user=$(stat -f "%Su" /dev/console)
-  if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then
+  if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
     echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'."
     eval "export $var_name=0"
     return
@@ -63,15 +65,28 @@
 
   local console_user
   console_user=$(stat -f "%Su" /dev/console)
-  if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then
+  if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
     echo "Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'."
     return
   fi
 
   echo "Relaunching application '$bundle_id'..."
 
-  # Try to launch the application
-  if osascript -e "tell application id \"$bundle_id\" to activate" >/dev/null 2>&1; then
+  # Launch the app in the logged-in user's GUI session. Apps launched by root
+  # won't register with the user's Dock/GUI, so run 'open' as the console user.
+  # Use 'launchctl asuser' to bootstrap into the console user's Mach namespace
+  # and GUI session — 'sudo -u' alone doesn't do this, which can cause
+  # LSOpenURLsWithRole() failures even when 'open' exits 0.
+  local open_status=0
+  if [[ $EUID -eq 0 ]]; then
+    local console_uid
+    console_uid=$(id -u "$console_user")
+    /bin/launchctl asuser "$console_uid" sudo -u "$console_user" open -b "$bundle_id" >/dev/null 2>&1 || open_status=$?
+  else
+    open -b "$bundle_id" >/dev/null 2>&1 || open_status=$?
+  fi
+
+  if [[ $open_status -eq 0 ]]; then
     echo "Application '$bundle_id' relaunched successfully."
   else
     echo "Failed to relaunch application '$bundle_id'."
@@ -81,9 +96,9 @@
 
 # extract contents
 MOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)
-hdiutil attach -plist -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$INSTALLER_PATH"
+yes | hdiutil attach -plist -nobrowse -readonly -mountpoint "$MOUNT_POINT" "$INSTALLER_PATH" || exit 1
 sudo cp -R "$MOUNT_POINT"/* "$TMPDIR"
-hdiutil detach "$MOUNT_POINT"
+hdiutil detach "$MOUNT_POINT" || true
 # copy to the applications folder
 quit_and_track_application 'com.vivaldi.Vivaldi'
 if [ -d "$APPDIR/Vivaldi.app" ]; then

=== Uninstall // 83339974 -> 7c1876ca ===

--- /tmp/old.2QSNqm	2026-06-16 15:17:52.177607518 +0000
+++ /tmp/new.IDXQPN	2026-06-16 15:17:52.178607562 +0000
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/bash
 
 # variables
 APPDIR="/Applications/"
@@ -10,13 +10,15 @@
   local timeout_duration=10
 
   # check if the application is running
-  if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then
+  local app_running
+  app_running=$(osascript -e "application id \"$bundle_id\" is running" 2>/dev/null)
+  if [[ "$app_running" != "true" ]]; then
     return
   fi
 
   local console_user
   console_user=$(stat -f "%Su" /dev/console)
-  if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then
+  if [[ -z "$console_user" || "$console_user" == "root" || "$console_user" == "loginwindow" ]]; then
     echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'."
     return
   fi
@@ -55,6 +57,31 @@
   fi
 
   local trash="/Users/$logged_in_user/.Trash"
+
+  # If the target contains glob characters, expand it and move each match.
+  if [[ "$target_file" == *[*?[]* ]]; then
+    local file file_name
+    local matched=false
+    local i=0
+    # compgen -G expands the (quoted) pattern itself, so paths containing
+    # spaces glob correctly; reading line by line keeps each match intact.
+    while IFS= read -r file; do
+      [[ -n "$file" ]] || continue
+      [[ -e "$file" || -L "$file" ]] || continue
+      matched=true
+      i=$((i + 1))
+      file_name="$(basename "$file")"
+      echo "removing $file."
+      # The per-match counter keeps matches that share a basename from
+      # overwriting each other in the trash.
+      mv -f "$file" "$trash/${file_name}_${timestamp}_${rand}_${i}"
+    done < <(compgen -G "$target_file" 2>/dev/null)
+    if [[ "$matched" == false ]]; then
+      echo "$target_file doesn't exist."
+    fi
+    return
+  fi
+
   local file_name="$(basename "${target_file}")"
 
   if [[ -e "$target_file" ]]; then

ee/maintained-apps/outputs/vivaldi/windows.json

=== Install Script (no changes) ===
=== Uninstall // 741609fc -> 7b14fec4 ===

--- /tmp/old.AH68g9	2026-06-16 15:17:52.218609327 +0000
+++ /tmp/new.5QgFGs	2026-06-16 15:17:52.218609327 +0000
@@ -3,17 +3,20 @@
 # fallback, then runs the Chromium uninstaller with --force-uninstall.
 
 $displayName = "Vivaldi"
+$publisher = "Vivaldi Technologies AS."
 
+# Install is machine-wide (--system-level), which registers under HKLM, so look
+# there first. HKCU is only a fallback for a stale user-level install.
 $paths = @(
-  'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
   'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall',
-  'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
+  'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall',
+  'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall'
 )
 
 $uninstall = $null
 foreach ($p in $paths) {
   $items = Get-ItemProperty "$p\*" -ErrorAction SilentlyContinue | Where-Object {
-    $_.DisplayName -and $_.DisplayName -eq $displayName
+    $_.DisplayName -eq $displayName -and $_.Publisher -eq $publisher
   }
   if ($items) { $uninstall = $items | Select-Object -First 1; break }
 }
@@ -33,10 +36,21 @@
   $uninstall.UninstallString
 }
 
-# Parse quoted executable from any trailing args in the registry string
-$splitArgs = $uninstallCommand.Split('"')
-$exe = $splitArgs[1]
-$existingArgs = if ($splitArgs.Length -eq 3) { $splitArgs[2].Trim() } else { "" }
+# Parse the executable + trailing args, handling the three registry shapes:
+# quoted, unquoted-with-spaces (capture through .exe), and a bare token.
+if ($uninstallCommand -match '^\s*"([^"]+)"\s*(.*)$') {
+  $exe = $Matches[1]
+  $existingArgs = $Matches[2].Trim()
+} elseif ($uninstallCommand -match '(?i)^\s*(.+?\.exe)\s*(.*)$') {
+  $exe = $Matches[1]
+  $existingArgs = $Matches[2].Trim()
+} elseif ($uninstallCommand -match '^\s*(\S+)\s*(.*)$') {
+  $exe = $Matches[1]
+  $existingArgs = $Matches[2].Trim()
+} else {
+  Write-Host "Unable to parse uninstall command: $uninstallCommand"
+  Exit 1
+}
 
 # Chromium-based uninstaller flags
 $uninstallArgs = "$existingArgs --uninstall --force-uninstall".Trim()

@allenhouchins
allenhouchins merged commit d17b10b into main Jun 18, 2026
50 checks passed
@allenhouchins
allenhouchins deleted the add-vivaldi-fma branch June 18, 2026 20:02
@coderabbitai coderabbitai Bot mentioned this pull request Jul 16, 2026
21 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants