Skip to content

Update Fleet-maintained apps#48583

Merged
allenhouchins merged 2 commits into
mainfrom
fma-2607011802
Jul 1, 2026
Merged

Update Fleet-maintained apps#48583
allenhouchins merged 2 commits into
mainfrom
fma-2607011802

Conversation

@fleet-release

@fleet-release fleet-release commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Automated ingestion of latest Fleet-maintained app data.

Summary by CodeRabbit

  • Bug Fixes
    • Improved uninstall reliability across many macOS apps, especially where background services use wildcard-style launch service names.
    • Uninstall cleanup now more accurately finds and removes matching services and their related plist/config/log files.
    • Added safer handling for cases like “no matching services found” and more robust app shutdown during uninstall.
  • Updates
    • Updated maintained app definitions and installer/uninstaller script references, including version refreshes for Dataflare, ElectronMail, Granola, and JASP (and related Windows installer checksums).

Generated automatically with cmd/maintained-apps.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: af9278df-7fc3-4eda-840f-e46a38cf908f

📥 Commits

Reviewing files that changed from the base of the PR and between dc97115 and 8398be1.

📒 Files selected for processing (1)
  • ee/maintained-apps/outputs/google-gemini/darwin.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • ee/maintained-apps/outputs/google-gemini/darwin.json

Walkthrough

This PR updates uninstall script references and embedded bash uninstall scripts across many maintained macOS app JSON outputs. The main script change is a new remove_launchctl_service implementation that expands wildcard service labels into matching currently loaded launchctl job labels before removing services and related plist files. It also includes a Cloudflare WARP uninstall flow replacement and separate version metadata bumps for Dataflare, ElectronMail, Filebeat, Granola, and JASP.

Changes

Area Change
macOS uninstall scripts uninstall_script_ref updates plus wildcard-aware remove_launchctl_service changes across many apps
Cloudflare WARP Uninstall script replaced with a quit/install/relaunch flow
Dataflare, ElectronMail, Filebeat, Granola, JASP Version, installer URL, sha256, and version-compare updates only

Sequence Diagram(s)

Not applicable.

Possibly related PRs

  • fleetdm/fleet#48533: Both PRs change macOS uninstall script behavior around remove_launchctl_service and wildcard service labels.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is far too vague and omits the required template sections, checklist items, and testing details. Replace it with the repository template, add the related issue, and complete or remove each checklist and testing item as applicable.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and broadly matches the main change: updating Fleet-maintained apps.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fma-2607011802

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ee/maintained-apps/outputs/aws-vpn-client/darwin.json (1)

11-20: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Handle unloaded wildcard launchd jobs before switching to this ref.

The wildcard branch only expands labels from launchctl list; if a matching plist exists but the job is not currently loaded, it returns before plist cleanup. That leaves LaunchAgent/LaunchDaemon plists behind and can let the service come back on next load/login. Please derive wildcard candidates from matching plist basenames as well, then regenerate the affected script refs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/maintained-apps/outputs/aws-vpn-client/darwin.json` around lines 11 - 20,
The wildcard handling in remove_launchctl_service only discovers labels from
launchctl list, so unloaded jobs with matching plists are skipped and their
plist files are left behind. Update the wildcard branch to also collect
candidates from matching LaunchAgent/LaunchDaemon plist basenames before
cleanup, then continue through the existing removal flow for each matched
service_label. After that, regenerate the affected script refs so the
darwin.json content stays in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ee/maintained-apps/outputs/box-drive/darwin.json`:
- Line 19: The uninstall script leaves the temporary sudoers file behind if it
exits early because the cleanup only happens at the end. Update the Box Drive
uninstall flow around the sudoers write and removal logic to register a trap
immediately after creating /etc/sudoers.d/box_uninstall, and have that trap
remove the file with sudo rm -f on exit or interruption. Keep the existing final
cleanup as well, but ensure the trap covers all paths in this script.
- Line 19: The wildcard expansion in expand_pkgid_and_map currently uses grep
with a regex prefix, so symbols like dots in com.box.desktop.installer.* can
match unintended receipts. Update expand_pkgid_and_map to use literal shell
prefix matching against the computed prefix when iterating pkgutil --pkgs
results, keeping remove_pkg_files and forget_pkg behavior limited to the exact
package namespace.

In `@ee/maintained-apps/outputs/breaktimer/darwin.json`:
- Line 19: The uninstall script currently removes a relative `breaktimer` path,
which can delete the wrong directory depending on the working directory. Update
the `darwin.json` uninstall commands to use the same fixed absolute location
created by the install step, or remove the symlink cleanup entirely if it is no
longer needed. Keep the change consistent with the surrounding
`remove_launchctl_service`, `quit_application`, and `trash` cleanup flow.

In `@ee/maintained-apps/outputs/cardhop/darwin.json`:
- Line 20: The generated uninstall-script template is missing an explicit
separator after the process-substitution done lines, causing the following
statements to run together and render invalid bash. Update the script generation
in the uninstall-script template used by the Cardhop ref so each done < <(...)
line is terminated with a semicolon before the next if/statement, then
regenerate the affected refs.

In `@ee/maintained-apps/outputs/crashplan/darwin.json`:
- Line 19: The wildcard handling in remove_launchctl_service only expands
against loaded launchctl labels, so unloaded but installed jobs are missed.
Update the wildcard branch to also scan the LaunchAgents/LaunchDaemons plist
paths for matching labels before returning, and merge those matches into
services so the cleanup loop can delete their plist files even when launchctl
list has no active entry.

In `@ee/maintained-apps/outputs/devin-desktop/darwin.json`:
- Line 19: The cleanup step in the shell script is using a quoted tilde path, so
`sudo rmdir '~/.codeium/windsurf'` will not expand to the logged-in user’s home
directory and leaves the directory behind. Update the removal logic near the
`APPDIR`, `LOGGED_IN_USER`, and `trash` usage to target the resolved user path
instead of a literal `~`, using the same user-derived path style already used
elsewhere in the script. Keep the fix scoped to the `rmdir` invocation so the
uninstall flow removes the intended directory.

In `@ee/maintained-apps/outputs/displaylink/darwin.json`:
- Line 20: The quit step is using a short app name instead of the full bundle
identifier, so `quit_application` may not target the same app instance
referenced elsewhere in the script. Update the `quit_application` call in the
DisplayLink cleanup flow to use the full identifier that matches `application
id`, specifically the `com.displaylink.DisplayLinkUserAgent` style value, so the
correct app is quit before files are removed.

In `@ee/maintained-apps/outputs/docker-desktop/darwin.json`:
- Line 20: The cleanup commands are using quoted tilde paths, so the shell never
expands them and the targeted cache directories are left behind. Update the two
sudo rmdir calls in the Docker Desktop uninstall script to use the explicit home
directory derived from LOGGED_IN_USER, matching the existing trash helper’s
/Users/$logged_in_user path handling, so the intended cache locations are
actually removed.

In `@ee/maintained-apps/outputs/dropbox/darwin.json`:
- Line 20: The wildcard handling in remove_launchctl_service only expands labels
from launchctl list, so unloaded matching LaunchAgent/Daemon plists are missed
and never removed. Update the wildcard branch to build services from both loaded
jobs and matching plist basenames under the LaunchAgents/LaunchDaemons locations
before returning on zero matches. Keep the existing matching logic around
services, plist_status, and the wildcard regex, but ensure unloaded plist files
are included in the delete pass.

In `@ee/maintained-apps/outputs/elgato-wave-link/darwin.json`:
- Line 20: The wildcard handling in remove_launchctl_service() bails out too
early when launchctl list finds no loaded jobs, which prevents matching
LaunchAgent/LaunchDaemon plist files from being removed. Update the wildcard
branch in remove_launchctl_service() to still perform a plist-glob cleanup
fallback for the matching service pattern before returning, then keep the
existing per-service removal flow for loaded matches. Ensure the fix preserves
the current matching logic used by launchctl list, service_label, and the paths
array so wildcard inputs fully clean up both loaded and unloaded artifacts.
- Line 20: The Wave Link teardown is using a wildcard bundle ID and a literal
tilde path, so the quit and LaunchAgent cleanup steps do not hit the intended
targets. Update the call to quit_application to pass the exact
com.elgato.WaveLink bundle identifier, and fix the user LaunchAgent removal to
use the logged-in user’s home directory rather than a quoted ~; locate the
affected invocations near remove_launchctl_service, quit_application, and the
sudo rm -rf cleanup lines.

In `@ee/maintained-apps/outputs/expressvpn/darwin.json`:
- Line 20: The uninstall script is quitting ExpressVPN with the wrong bundle
identifier, so the running GUI app may be missed before removal. Update the call
to quit_application in the uninstall flow to use the same actual bundle ID used
elsewhere in the script and install logic, com.expressvpn.ExpressVPN, so the app
is correctly detected and closed before deleting the bundle.

In `@ee/maintained-apps/outputs/fig/darwin.json`:
- Line 20: The uninstall hook invocation is using a single-quoted executable
path, so the APPDIR variable is not expanded and the brew-uninstall command
never runs. Update the Fig uninstall call near the existing fig-darwin-universal
execution so the path is evaluated with the APPDIR value before invoking the
hook, and keep the surrounding LOGGED_IN_USER handling unchanged.

In `@ee/maintained-apps/outputs/malwarebytes/darwin.json`:
- Line 19: The uninstall script only quits the agent and leaves the main
Malwarebytes UI bundle running, so stop the application bundle too before file
cleanup. Update the uninstall flow near
quit_application/remove_launchctl_service to also call quit_application for
com.malwarebytes.mbam.frontend.application, using the existing quit_application
helper so the app bundle is terminated before remove_pkg_files, forget_pkg, and
trash run.

In `@ee/maintained-apps/outputs/microsoft-auto-update/darwin.json`:
- Line 19: The AutoUpdate uninstaller is deleting shared Microsoft group
containers that other Microsoft apps rely on. Update the cleanup logic in the
script around trash() calls so `UBF8T346G9.com.microsoft.oneauth` and
`UBF8T346G9.ms` are not removed by default; restrict cleanup to
AutoUpdate-specific paths or add a check before those removals to only proceed
when no other Microsoft apps are installed.

In `@ee/maintained-apps/outputs/microsoft-edge/darwin.json`:
- Line 19: The EdgeUpdater cleanup still targets only a single fixed label, so
suffixed jobs like the wake.system and other update variants are left behind.
Update the Edge removal flow in remove_launchctl_service to use the
wildcard-capable path already implemented there, and change the EdgeUpdater
invocation to pass the broader label pattern so it expands to all matching
loaded services before removal.

---

Outside diff comments:
In `@ee/maintained-apps/outputs/aws-vpn-client/darwin.json`:
- Around line 11-20: The wildcard handling in remove_launchctl_service only
discovers labels from launchctl list, so unloaded jobs with matching plists are
skipped and their plist files are left behind. Update the wildcard branch to
also collect candidates from matching LaunchAgent/LaunchDaemon plist basenames
before cleanup, then continue through the existing removal flow for each matched
service_label. After that, regenerate the affected script refs so the
darwin.json content stays in sync.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f9f270fe-88b7-4071-8f53-cc0760ab699e

📥 Commits

Reviewing files that changed from the base of the PR and between 257d9a0 and dc97115.

📒 Files selected for processing (120)
  • ee/maintained-apps/outputs/adguard/darwin.json
  • ee/maintained-apps/outputs/adlock/darwin.json
  • ee/maintained-apps/outputs/adobe-acrobat-reader/darwin.json
  • ee/maintained-apps/outputs/aldente/darwin.json
  • ee/maintained-apps/outputs/amazon-workspaces/darwin.json
  • ee/maintained-apps/outputs/anka-virtualization/darwin.json
  • ee/maintained-apps/outputs/appcleaner/darwin.json
  • ee/maintained-apps/outputs/aviatrix-vpn-client/darwin.json
  • ee/maintained-apps/outputs/aws-vpn-client/darwin.json
  • ee/maintained-apps/outputs/background-music/darwin.json
  • ee/maintained-apps/outputs/bartender/darwin.json
  • ee/maintained-apps/outputs/boom-3d/darwin.json
  • ee/maintained-apps/outputs/box-drive/darwin.json
  • ee/maintained-apps/outputs/breaktimer/darwin.json
  • ee/maintained-apps/outputs/cardhop/darwin.json
  • ee/maintained-apps/outputs/charles/darwin.json
  • ee/maintained-apps/outputs/chrome-remote-desktop-host/darwin.json
  • ee/maintained-apps/outputs/citrix-workspace/darwin.json
  • ee/maintained-apps/outputs/clocker/darwin.json
  • ee/maintained-apps/outputs/cloudflare-warp/darwin.json
  • ee/maintained-apps/outputs/coconutbattery/darwin.json
  • ee/maintained-apps/outputs/crashplan/darwin.json
  • ee/maintained-apps/outputs/daisydisk/darwin.json
  • ee/maintained-apps/outputs/dataflare/darwin.json
  • ee/maintained-apps/outputs/dataflare/windows.json
  • ee/maintained-apps/outputs/debookee/darwin.json
  • ee/maintained-apps/outputs/devin-desktop/darwin.json
  • ee/maintained-apps/outputs/dfu-blaster-pro/darwin.json
  • ee/maintained-apps/outputs/displaylink/darwin.json
  • ee/maintained-apps/outputs/docker-desktop/darwin.json
  • ee/maintained-apps/outputs/dropbox/darwin.json
  • ee/maintained-apps/outputs/druva-insync/darwin.json
  • ee/maintained-apps/outputs/duo-desktop/darwin.json
  • ee/maintained-apps/outputs/dymo-connect/darwin.json
  • ee/maintained-apps/outputs/electronmail/darwin.json
  • ee/maintained-apps/outputs/elgato-camera-hub/darwin.json
  • ee/maintained-apps/outputs/elgato-wave-link/darwin.json
  • ee/maintained-apps/outputs/expressvpn/darwin.json
  • ee/maintained-apps/outputs/fig/darwin.json
  • ee/maintained-apps/outputs/filebeat/windows.json
  • ee/maintained-apps/outputs/fing/darwin.json
  • ee/maintained-apps/outputs/focusrite-control-2/darwin.json
  • ee/maintained-apps/outputs/forklift/darwin.json
  • ee/maintained-apps/outputs/free-download-manager/darwin.json
  • ee/maintained-apps/outputs/fsmonitor/darwin.json
  • ee/maintained-apps/outputs/gitfinder/darwin.json
  • ee/maintained-apps/outputs/gog-galaxy/darwin.json
  • ee/maintained-apps/outputs/google-chrome/darwin.json
  • ee/maintained-apps/outputs/google-drive/darwin.json
  • ee/maintained-apps/outputs/google-earth-pro/darwin.json
  • ee/maintained-apps/outputs/google-gemini/darwin.json
  • ee/maintained-apps/outputs/granola/darwin.json
  • ee/maintained-apps/outputs/granola/windows.json
  • ee/maintained-apps/outputs/gyazo/darwin.json
  • ee/maintained-apps/outputs/hazeover/darwin.json
  • ee/maintained-apps/outputs/hiddenbar/darwin.json
  • ee/maintained-apps/outputs/i1profiler/darwin.json
  • ee/maintained-apps/outputs/jasp/darwin.json
  • ee/maintained-apps/outputs/jetbrains-toolbox/darwin.json
  • ee/maintained-apps/outputs/klokki/darwin.json
  • ee/maintained-apps/outputs/malwarebytes/darwin.json
  • ee/maintained-apps/outputs/megasync/darwin.json
  • ee/maintained-apps/outputs/microsoft-auto-update/darwin.json
  • ee/maintained-apps/outputs/microsoft-edge/darwin.json
  • ee/maintained-apps/outputs/microsoft-excel/darwin.json
  • ee/maintained-apps/outputs/microsoft-outlook/darwin.json
  • ee/maintained-apps/outputs/microsoft-powerpoint/darwin.json
  • ee/maintained-apps/outputs/microsoft-teams/darwin.json
  • ee/maintained-apps/outputs/mist/darwin.json
  • ee/maintained-apps/outputs/mullvad-vpn/darwin.json
  • ee/maintained-apps/outputs/ndi-tools/darwin.json
  • ee/maintained-apps/outputs/nextcloud/darwin.json
  • ee/maintained-apps/outputs/nordlayer/darwin.json
  • ee/maintained-apps/outputs/nordpass/darwin.json
  • ee/maintained-apps/outputs/nordvpn/darwin.json
  • ee/maintained-apps/outputs/numi/darwin.json
  • ee/maintained-apps/outputs/okta-verify/darwin.json
  • ee/maintained-apps/outputs/omnissa-horizon-client/darwin.json
  • ee/maintained-apps/outputs/onedrive/darwin.json
  • ee/maintained-apps/outputs/openvpn-connect/darwin.json
  • ee/maintained-apps/outputs/plex-media-server/darwin.json
  • ee/maintained-apps/outputs/postgres-app/darwin.json
  • ee/maintained-apps/outputs/power-monitor/darwin.json
  • ee/maintained-apps/outputs/pritunl/darwin.json
  • ee/maintained-apps/outputs/privileges/darwin.json
  • ee/maintained-apps/outputs/proton-mail-bridge/darwin.json
  • ee/maintained-apps/outputs/protonvpn/darwin.json
  • ee/maintained-apps/outputs/proxyman/darwin.json
  • ee/maintained-apps/outputs/radio-silence/darwin.json
  • ee/maintained-apps/outputs/santa/darwin.json
  • ee/maintained-apps/outputs/sensei/darwin.json
  • ee/maintained-apps/outputs/sharefile/darwin.json
  • ee/maintained-apps/outputs/shifty/darwin.json
  • ee/maintained-apps/outputs/sound-control/darwin.json
  • ee/maintained-apps/outputs/sourcetree/darwin.json
  • ee/maintained-apps/outputs/splashtop-business/darwin.json
  • ee/maintained-apps/outputs/splashtop-streamer/darwin.json
  • ee/maintained-apps/outputs/stats/darwin.json
  • ee/maintained-apps/outputs/steam/darwin.json
  • ee/maintained-apps/outputs/surge/darwin.json
  • ee/maintained-apps/outputs/teamviewer/darwin.json
  • ee/maintained-apps/outputs/tiles/darwin.json
  • ee/maintained-apps/outputs/tripmode/darwin.json
  • ee/maintained-apps/outputs/tunnelblick/darwin.json
  • ee/maintained-apps/outputs/tuple/darwin.json
  • ee/maintained-apps/outputs/twingate/darwin.json
  • ee/maintained-apps/outputs/ua-connect/darwin.json
  • ee/maintained-apps/outputs/viscosity/darwin.json
  • ee/maintained-apps/outputs/visual-studio-code/darwin.json
  • ee/maintained-apps/outputs/vnc-server/darwin.json
  • ee/maintained-apps/outputs/vyprvpn/darwin.json
  • ee/maintained-apps/outputs/whatroute/darwin.json
  • ee/maintained-apps/outputs/wifiman/darwin.json
  • ee/maintained-apps/outputs/windows-app/darwin.json
  • ee/maintained-apps/outputs/wireshark-app/darwin.json
  • ee/maintained-apps/outputs/workflowy/darwin.json
  • ee/maintained-apps/outputs/xcreds/darwin.json
  • ee/maintained-apps/outputs/xquartz/darwin.json
  • ee/maintained-apps/outputs/zoom-rooms/darwin.json
  • ee/maintained-apps/outputs/zoom/darwin.json

"refs": {
"6e4fcb5d": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath \"$INSTALLER_PATH\")\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Launch the app in the logged-in user's GUI session. Apps launched by root\n # won't register with the user's Dock/GUI, so run 'open' as the console user.\n # Use 'launchctl asuser' to bootstrap into the console user's Mach namespace\n # and GUI session — 'sudo -u' alone doesn't do this, which can cause\n # LSOpenURLsWithRole() failures even when 'open' exits 0.\n local open_status=0\n if [[ $EUID -eq 0 ]]; then\n local console_uid\n console_uid=$(id -u \"$console_user\")\n /bin/launchctl asuser \"$console_uid\" sudo -u \"$console_user\" open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n else\n open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n fi\n\n if [[ $open_status -eq 0 ]]; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# install pkg files\nquit_and_track_application 'com.box.desktop'\nsudo installer -pkg \"$TMPDIR/BoxDrive-2.52.312.pkg\" -target /\nrelaunch_application 'com.box.desktop'\n",
"7b684425": "#!/bin/bash\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nexpand_pkgid_and_map() {\n local PKGID=\"$1\"\n local FUNC=\"$2\"\n if [[ \"$PKGID\" == *\"*\" ]]; then\n local prefix=\"${PKGID%\\*}\"\n echo \"Expanding wildcard for PKGID: $PKGID\"\n for receipt in $(pkgutil --pkgs | grep \"^${prefix}\"); do\n echo \"Processing $receipt\"\n \"$FUNC\" \"$receipt\"\n done\n else\n \"$FUNC\" \"$PKGID\"\n fi\n}\n\nforget_pkg() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" forget_receipt\n}\n\nforget_receipt() {\n local PKGID=\"$1\"\n sudo pkgutil --forget \"$PKGID\"\n}\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service}\"\n else\n launchctl remove \"${service}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service}.plist\"\n \"/Library/LaunchDaemons/${service}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n}\n\nremove_pkg_files() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" remove_receipt_files\n}\n\nremove_receipt_files() {\n local PKGID=\"$1\"\n local PKGINFO VOLUME INSTALL_LOCATION FULL_INSTALL_LOCATION\n\n echo \"pkgutil --pkg-info-plist \\\"$PKGID\\\"\"\n PKGINFO=$(pkgutil --pkg-info-plist \"$PKGID\")\n VOLUME=$(echo \"$PKGINFO\" | awk '/<key>volume<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n INSTALL_LOCATION=$(echo \"$PKGINFO\" | awk '/<key>install-location<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n\n if [ -z \"$INSTALL_LOCATION\" ] || [ \"$INSTALL_LOCATION\" = \"/\" ]; then\n FULL_INSTALL_LOCATION=\"$VOLUME\"\n else\n FULL_INSTALL_LOCATION=\"$VOLUME/$INSTALL_LOCATION\"\n FULL_INSTALL_LOCATION=$(echo \"$FULL_INSTALL_LOCATION\" | sed 's|//|/|g')\n fi\n\n echo \"sudo pkgutil --only-files --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-files --files \"$PKGID\" | sed \"s|^|/${INSTALL_LOCATION}/|\" | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n echo \"sudo pkgutil --only-dirs --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | grep '\\\\.app$' | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-dirs --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | grep '\\.app$' | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n root_app_dir=$(\n sudo pkgutil --only-dirs --files \"$PKGID\" \\\n | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" \\\n | grep 'Applications' \\\n | awk '{ print length, $0 }' \\\n | sort -n \\\n | head -n1 \\\n | cut -d' ' -f2-\n )\n if [ -n \"$root_app_dir\" ]; then\n echo \"sudo rmdir -p \\\"$root_app_dir\\\" 2>/dev/null || :\"\n sudo rmdir -p \"$root_app_dir\" 2>/dev/null || :\n fi\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\n(cd /Users/$LOGGED_IN_USER; sudo -u $LOGGED_IN_USER fileproviderctl domain remove -A com.box.desktop.boxfileprovider)\n(cd /Users/$LOGGED_IN_USER; sudo -u $LOGGED_IN_USER /Applications/Box.app/Contents/MacOS/fpe/streem --remove-fpe-domain-and-archive-unsynced-content Box)\n(cd /Users/$LOGGED_IN_USER; sudo -u $LOGGED_IN_USER /Applications/Box.app/Contents/MacOS/fpe/streem --remove-fpe-domain-and-preserve-unsynced-content Box)\n(cd /Users/$LOGGED_IN_USER; defaults delete com.box.desktop)\necho \"${LOGGED_IN_USER} ALL = (root) NOPASSWD: /Library/Application\\ Support/Box/uninstall_box_drive_r\" >> /etc/sudoers.d/box_uninstall\nremove_launchctl_service 'com.box.desktop.helper'\nquit_application 'com.box.Box-Local-Com-Server'\nquit_application 'com.box.desktop'\nquit_application 'com.box.desktop.findersyncext'\nquit_application 'com.box.desktop.helper'\nquit_application 'com.box.desktop.ui'\n(cd /Users/$LOGGED_IN_USER && sudo -u \"$LOGGED_IN_USER\" '/Library/Application Support/Box/uninstall_box_drive')\nremove_pkg_files 'com.box.desktop.installer.*'\nforget_pkg 'com.box.desktop.installer.*'\nrm /etc/sudoers.d/box_uninstall\ntrash $LOGGED_IN_USER '~/.Box_*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Box/Box'\ntrash $LOGGED_IN_USER '~/Library/Application Support/FileProvider/com.box.desktop.boxfileprovider'\ntrash $LOGGED_IN_USER '~/Library/Containers/com.box.desktop.findersyncext'\ntrash $LOGGED_IN_USER '~/Library/Logs/Box/Box'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.box.desktop.plist'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.box.desktop.ui.plist'\n"
"0b49ca2e": "#!/bin/bash\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nexpand_pkgid_and_map() {\n local PKGID=\"$1\"\n local FUNC=\"$2\"\n if [[ \"$PKGID\" == *\"*\" ]]; then\n local prefix=\"${PKGID%\\*}\"\n echo \"Expanding wildcard for PKGID: $PKGID\"\n for receipt in $(pkgutil --pkgs | grep \"^${prefix}\"); do\n echo \"Processing $receipt\"\n \"$FUNC\" \"$receipt\"\n done\n else\n \"$FUNC\" \"$PKGID\"\n fi\n}\n\nforget_pkg() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" forget_receipt\n}\n\nforget_receipt() {\n local PKGID=\"$1\"\n sudo pkgutil --forget \"$PKGID\"\n}\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n # A wildcard label can't be used with launchctl or as a plist name, so expand\n # it to the labels of currently loaded services that match the pattern.\n local services=(\"$service\")\n if [[ \"$service\" == *\"*\"* ]]; then\n local regex\n # Escape regex metacharacters, turn '*' into '.*', and anchor the pattern so\n # it matches a full label rather than a substring.\n regex=$(printf '%s' \"$service\" | sed -e 's/[][(){}.^$+?|\\\\]/\\\\&/g' -e 's/\\*/.*/g')\n regex=\"^${regex}$\"\n services=()\n local id\n # Match every loaded job by label regardless of PID; launchctl list reports\n # loaded-but-not-running jobs with a \"-\" in the PID column.\n while read -r _ _ id; do\n [[ \"$id\" =~ $regex ]] && services+=(\"$id\")\n done < <(launchctl list 2>/dev/null | tail -n +2)\n if [[ ${#services[@]} -eq 0 ]]; then\n echo \"No loaded launchctl service matches ${service}\"\n return\n fi\n fi\n\n local service_label\n for service_label in \"${services[@]}\"; do\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service_label}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service_label}\"\n else\n launchctl remove \"${service_label}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service_label}.plist\"\n \"/Library/LaunchDaemons/${service_label}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n done\n}\n\nremove_pkg_files() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" remove_receipt_files\n}\n\nremove_receipt_files() {\n local PKGID=\"$1\"\n local PKGINFO VOLUME INSTALL_LOCATION FULL_INSTALL_LOCATION\n\n echo \"pkgutil --pkg-info-plist \\\"$PKGID\\\"\"\n PKGINFO=$(pkgutil --pkg-info-plist \"$PKGID\")\n VOLUME=$(echo \"$PKGINFO\" | awk '/<key>volume<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n INSTALL_LOCATION=$(echo \"$PKGINFO\" | awk '/<key>install-location<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n\n if [ -z \"$INSTALL_LOCATION\" ] || [ \"$INSTALL_LOCATION\" = \"/\" ]; then\n FULL_INSTALL_LOCATION=\"$VOLUME\"\n else\n FULL_INSTALL_LOCATION=\"$VOLUME/$INSTALL_LOCATION\"\n FULL_INSTALL_LOCATION=$(echo \"$FULL_INSTALL_LOCATION\" | sed 's|//|/|g')\n fi\n\n echo \"sudo pkgutil --only-files --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-files --files \"$PKGID\" | sed \"s|^|/${INSTALL_LOCATION}/|\" | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n echo \"sudo pkgutil --only-dirs --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | grep '\\\\.app$' | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-dirs --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | grep '\\.app$' | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n root_app_dir=$(\n sudo pkgutil --only-dirs --files \"$PKGID\" \\\n | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" \\\n | grep 'Applications' \\\n | awk '{ print length, $0 }' \\\n | sort -n \\\n | head -n1 \\\n | cut -d' ' -f2-\n )\n if [ -n \"$root_app_dir\" ]; then\n echo \"sudo rmdir -p \\\"$root_app_dir\\\" 2>/dev/null || :\"\n sudo rmdir -p \"$root_app_dir\" 2>/dev/null || :\n fi\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\n(cd /Users/$LOGGED_IN_USER; sudo -u $LOGGED_IN_USER fileproviderctl domain remove -A com.box.desktop.boxfileprovider)\n(cd /Users/$LOGGED_IN_USER; sudo -u $LOGGED_IN_USER /Applications/Box.app/Contents/MacOS/fpe/streem --remove-fpe-domain-and-archive-unsynced-content Box)\n(cd /Users/$LOGGED_IN_USER; sudo -u $LOGGED_IN_USER /Applications/Box.app/Contents/MacOS/fpe/streem --remove-fpe-domain-and-preserve-unsynced-content Box)\n(cd /Users/$LOGGED_IN_USER; defaults delete com.box.desktop)\necho \"${LOGGED_IN_USER} ALL = (root) NOPASSWD: /Library/Application\\ Support/Box/uninstall_box_drive_r\" >> /etc/sudoers.d/box_uninstall\nremove_launchctl_service 'com.box.desktop.helper'\nquit_application 'com.box.Box-Local-Com-Server'\nquit_application 'com.box.desktop'\nquit_application 'com.box.desktop.findersyncext'\nquit_application 'com.box.desktop.helper'\nquit_application 'com.box.desktop.ui'\n(cd /Users/$LOGGED_IN_USER && sudo -u \"$LOGGED_IN_USER\" '/Library/Application Support/Box/uninstall_box_drive')\nremove_pkg_files 'com.box.desktop.installer.*'\nforget_pkg 'com.box.desktop.installer.*'\nrm /etc/sudoers.d/box_uninstall\ntrash $LOGGED_IN_USER '~/.Box_*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Box/Box'\ntrash $LOGGED_IN_USER '~/Library/Application Support/FileProvider/com.box.desktop.boxfileprovider'\ntrash $LOGGED_IN_USER '~/Library/Containers/com.box.desktop.findersyncext'\ntrash $LOGGED_IN_USER '~/Library/Logs/Box/Box'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.box.desktop.plist'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.box.desktop.ui.plist'\n",

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Clean up the temporary sudoers rule with a trap.

The script adds a NOPASSWD rule and only removes it at the end. If the uninstall command exits early or the process is interrupted, /etc/sudoers.d/box_uninstall can persist and leave unnecessary privilege elevation behind. Add a trap immediately after creating the file and remove it with sudo rm -f.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/maintained-apps/outputs/box-drive/darwin.json` at line 19, The uninstall
script leaves the temporary sudoers file behind if it exits early because the
cleanup only happens at the end. Update the Box Drive uninstall flow around the
sudoers write and removal logic to register a trap immediately after creating
/etc/sudoers.d/box_uninstall, and have that trap remove the file with sudo rm -f
on exit or interruption. Keep the existing final cleanup as well, but ensure the
trap covers all paths in this script.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use literal prefix matching for package wildcards.

grep "^${prefix}" treats dots in com.box.desktop.installer. as regex wildcards, so remove_pkg_files 'com.box.desktop.installer.*' can match unrelated receipts before deleting files. Use shell literal-prefix matching instead, e.g. [[ "$receipt" == "$prefix"* ]].

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/maintained-apps/outputs/box-drive/darwin.json` at line 19, The wildcard
expansion in expand_pkgid_and_map currently uses grep with a regex prefix, so
symbols like dots in com.box.desktop.installer.* can match unintended receipts.
Update expand_pkgid_and_map to use literal shell prefix matching against the
computed prefix when iterating pkgutil --pkgs results, keeping remove_pkg_files
and forget_pkg behavior limited to the exact package namespace.

],
"refs": {
"591f7cb3": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service}\"\n else\n launchctl remove \"${service}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service}.plist\"\n \"/Library/LaunchDaemons/${service}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.tomjwatson.breaktimer.ShipIt'\nquit_application 'com.tomjwatson.breaktimer'\nsudo rm -rf \"$APPDIR/BreakTimer.app\"\nsudo rm -rf 'breaktimer'\ntrash $LOGGED_IN_USER '~/Library/Application Support/BreakTimer'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.tomjwatson.breaktimer'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.tomjwatson.breaktimer.ShipIt'\ntrash $LOGGED_IN_USER '~/Library/Logs/BreakTimer'\ntrash $LOGGED_IN_USER '~/Library/Preferences/ByHost/com.tomjwatson.breaktimer.ShipIt.*.plist'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.tomjwatson.breaktimer.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/com.tomjwatson.breaktimer.savedState'\n",
"61fd8c97": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n # A wildcard label can't be used with launchctl or as a plist name, so expand\n # it to the labels of currently loaded services that match the pattern.\n local services=(\"$service\")\n if [[ \"$service\" == *\"*\"* ]]; then\n local regex\n # Escape regex metacharacters, turn '*' into '.*', and anchor the pattern so\n # it matches a full label rather than a substring.\n regex=$(printf '%s' \"$service\" | sed -e 's/[][(){}.^$+?|\\\\]/\\\\&/g' -e 's/\\*/.*/g')\n regex=\"^${regex}$\"\n services=()\n local id\n # Match every loaded job by label regardless of PID; launchctl list reports\n # loaded-but-not-running jobs with a \"-\" in the PID column.\n while read -r _ _ id; do\n [[ \"$id\" =~ $regex ]] && services+=(\"$id\")\n done < <(launchctl list 2>/dev/null | tail -n +2)\n if [[ ${#services[@]} -eq 0 ]]; then\n echo \"No loaded launchctl service matches ${service}\"\n return\n fi\n fi\n\n local service_label\n for service_label in \"${services[@]}\"; do\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service_label}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service_label}\"\n else\n launchctl remove \"${service_label}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service_label}.plist\"\n \"/Library/LaunchDaemons/${service_label}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.tomjwatson.breaktimer.ShipIt'\nquit_application 'com.tomjwatson.breaktimer'\nsudo rm -rf \"$APPDIR/BreakTimer.app\"\nsudo rm -rf 'breaktimer'\ntrash $LOGGED_IN_USER '~/Library/Application Support/BreakTimer'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.tomjwatson.breaktimer'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.tomjwatson.breaktimer.ShipIt'\ntrash $LOGGED_IN_USER '~/Library/Logs/BreakTimer'\ntrash $LOGGED_IN_USER '~/Library/Preferences/ByHost/com.tomjwatson.breaktimer.ShipIt.*.plist'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.tomjwatson.breaktimer.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/com.tomjwatson.breaktimer.savedState'\n",

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Avoid deleting a relative breaktimer path.

sudo rm -rf 'breaktimer' targets whatever the current working directory happens to be during uninstall. Use a fixed absolute path shared with the install step, or stop creating/removing this relative symlink.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/maintained-apps/outputs/breaktimer/darwin.json` at line 19, The uninstall
script currently removes a relative `breaktimer` path, which can delete the
wrong directory depending on the working directory. Update the `darwin.json`
uninstall commands to use the same fixed absolute location created by the
install step, or remove the symlink cleanup entirely if it is no longer needed.
Keep the change consistent with the surrounding `remove_launchctl_service`,
`quit_application`, and `trash` cleanup flow.

"0b17397d": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service}\"\n else\n launchctl remove \"${service}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service}.plist\"\n \"/Library/LaunchDaemons/${service}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.flexibits.cardhop.mac.launcher'\nquit_application 'com.flexibits.cardhop.mac'\nsudo rm -rf \"$APPDIR/Cardhop.app\"\ntrash $LOGGED_IN_USER '~/Library/Application Scripts/com.flexibits.cardhop.mac'\ntrash $LOGGED_IN_USER '~/Library/Application Scripts/com.flexibits.cardhop.mac.BluetoothDialer'\ntrash $LOGGED_IN_USER '~/Library/Application Scripts/com.flexibits.cardhop.mac.launcher'\ntrash $LOGGED_IN_USER '~/Library/Containers/com.flexibits.cardhop.mac'\ntrash $LOGGED_IN_USER '~/Library/Containers/com.flexibits.cardhop.mac.BluetoothDialer'\ntrash $LOGGED_IN_USER '~/Library/Containers/com.flexibits.cardhop.mac.launcher'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.flexibits.cardhop.mac.plist'\n",
"30166eaf": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath \"$INSTALLER_PATH\")\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Launch the app in the logged-in user's GUI session. Apps launched by root\n # won't register with the user's Dock/GUI, so run 'open' as the console user.\n # Use 'launchctl asuser' to bootstrap into the console user's Mach namespace\n # and GUI session — 'sudo -u' alone doesn't do this, which can cause\n # LSOpenURLsWithRole() failures even when 'open' exits 0.\n local open_status=0\n if [[ $EUID -eq 0 ]]; then\n local console_uid\n console_uid=$(id -u \"$console_user\")\n /bin/launchctl asuser \"$console_uid\" sudo -u \"$console_user\" open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n else\n open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n fi\n\n if [[ $open_status -eq 0 ]]; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# extract contents\nunzip \"$INSTALLER_PATH\" -d \"$TMPDIR\"\n# copy to the applications folder\nquit_and_track_application 'com.flexibits.cardhop.mac'\nif [ -d \"$APPDIR/Cardhop.app\" ]; then\n\tsudo mv \"$APPDIR/Cardhop.app\" \"$TMPDIR/Cardhop.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Cardhop.app\" \"$APPDIR\"\nrelaunch_application 'com.flexibits.cardhop.mac'\n"
"30166eaf": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath \"$INSTALLER_PATH\")\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Launch the app in the logged-in user's GUI session. Apps launched by root\n # won't register with the user's Dock/GUI, so run 'open' as the console user.\n # Use 'launchctl asuser' to bootstrap into the console user's Mach namespace\n # and GUI session — 'sudo -u' alone doesn't do this, which can cause\n # LSOpenURLsWithRole() failures even when 'open' exits 0.\n local open_status=0\n if [[ $EUID -eq 0 ]]; then\n local console_uid\n console_uid=$(id -u \"$console_user\")\n /bin/launchctl asuser \"$console_uid\" sudo -u \"$console_user\" open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n else\n open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n fi\n\n if [[ $open_status -eq 0 ]]; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# extract contents\nunzip \"$INSTALLER_PATH\" -d \"$TMPDIR\"\n# copy to the applications folder\nquit_and_track_application 'com.flexibits.cardhop.mac'\nif [ -d \"$APPDIR/Cardhop.app\" ]; then\n\tsudo mv \"$APPDIR/Cardhop.app\" \"$TMPDIR/Cardhop.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Cardhop.app\" \"$APPDIR\"\nrelaunch_application 'com.flexibits.cardhop.mac'\n",
"3fc281ff": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n # A wildcard label can't be used with launchctl or as a plist name, so expand\n # it to the labels of currently loaded services that match the pattern.\n local services=(\"$service\")\n if [[ \"$service\" == *\"*\"* ]]; then\n local regex\n # Escape regex metacharacters, turn '*' into '.*', and anchor the pattern so\n # it matches a full label rather than a substring.\n regex=$(printf '%s' \"$service\" | sed -e 's/[][(){}.^$+?|\\\\]/\\\\&/g' -e 's/\\*/.*/g')\n regex=\"^${regex}$\"\n services=()\n local id\n # Match every loaded job by label regardless of PID; launchctl list reports\n # loaded-but-not-running jobs with a \"-\" in the PID column.\n while read -r _ _ id; do\n [[ \"$id\" =~ $regex ]] && services+=(\"$id\")\n done < <(launchctl list 2>/dev/null | tail -n +2)\n if [[ ${#services[@]} -eq 0 ]]; then\n echo \"No loaded launchctl service matches ${service}\"\n return\n fi\n fi\n\n local service_label\n for service_label in \"${services[@]}\"; do\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service_label}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service_label}\"\n else\n launchctl remove \"${service_label}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service_label}.plist\"\n \"/Library/LaunchDaemons/${service_label}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.flexibits.cardhop.mac.launcher'\nquit_application 'com.flexibits.cardhop.mac'\nsudo rm -rf \"$APPDIR/Cardhop.app\"\ntrash $LOGGED_IN_USER '~/Library/Application Scripts/com.flexibits.cardhop.mac'\ntrash $LOGGED_IN_USER '~/Library/Application Scripts/com.flexibits.cardhop.mac.BluetoothDialer'\ntrash $LOGGED_IN_USER '~/Library/Application Scripts/com.flexibits.cardhop.mac.launcher'\ntrash $LOGGED_IN_USER '~/Library/Containers/com.flexibits.cardhop.mac'\ntrash $LOGGED_IN_USER '~/Library/Containers/com.flexibits.cardhop.mac.BluetoothDialer'\ntrash $LOGGED_IN_USER '~/Library/Containers/com.flexibits.cardhop.mac.launcher'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.flexibits.cardhop.mac.plist'\n"

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Add an explicit separator after process-substitution done lines before regenerating refs.

website/scripts/build-static-content.js:1482-1525 condenses these functions onto one line and does not append semicolons to lines starting with done. This makes lines like done < <(launchctl list ...) join directly with the following if, producing invalid rendered bash. Add ; after each done < <(...) in the generated uninstall-script template, then regenerate all affected refs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/maintained-apps/outputs/cardhop/darwin.json` at line 20, The generated
uninstall-script template is missing an explicit separator after the
process-substitution done lines, causing the following statements to run
together and render invalid bash. Update the script generation in the
uninstall-script template used by the Cardhop ref so each done < <(...) line is
terminated with a semicolon before the next if/statement, then regenerate the
affected refs.

],
"refs": {
"095dbea6": "#!/bin/bash\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nexpand_pkgid_and_map() {\n local PKGID=\"$1\"\n local FUNC=\"$2\"\n if [[ \"$PKGID\" == *\"*\" ]]; then\n local prefix=\"${PKGID%\\*}\"\n echo \"Expanding wildcard for PKGID: $PKGID\"\n for receipt in $(pkgutil --pkgs | grep \"^${prefix}\"); do\n echo \"Processing $receipt\"\n \"$FUNC\" \"$receipt\"\n done\n else\n \"$FUNC\" \"$PKGID\"\n fi\n}\n\nforget_pkg() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" forget_receipt\n}\n\nforget_receipt() {\n local PKGID=\"$1\"\n sudo pkgutil --forget \"$PKGID\"\n}\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service}\"\n else\n launchctl remove \"${service}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service}.plist\"\n \"/Library/LaunchDaemons/${service}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n}\n\nremove_pkg_files() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" remove_receipt_files\n}\n\nremove_receipt_files() {\n local PKGID=\"$1\"\n local PKGINFO VOLUME INSTALL_LOCATION FULL_INSTALL_LOCATION\n\n echo \"pkgutil --pkg-info-plist \\\"$PKGID\\\"\"\n PKGINFO=$(pkgutil --pkg-info-plist \"$PKGID\")\n VOLUME=$(echo \"$PKGINFO\" | awk '/<key>volume<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n INSTALL_LOCATION=$(echo \"$PKGINFO\" | awk '/<key>install-location<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n\n if [ -z \"$INSTALL_LOCATION\" ] || [ \"$INSTALL_LOCATION\" = \"/\" ]; then\n FULL_INSTALL_LOCATION=\"$VOLUME\"\n else\n FULL_INSTALL_LOCATION=\"$VOLUME/$INSTALL_LOCATION\"\n FULL_INSTALL_LOCATION=$(echo \"$FULL_INSTALL_LOCATION\" | sed 's|//|/|g')\n fi\n\n echo \"sudo pkgutil --only-files --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-files --files \"$PKGID\" | sed \"s|^|/${INSTALL_LOCATION}/|\" | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n echo \"sudo pkgutil --only-dirs --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | grep '\\\\.app$' | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-dirs --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | grep '\\.app$' | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n root_app_dir=$(\n sudo pkgutil --only-dirs --files \"$PKGID\" \\\n | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" \\\n | grep 'Applications' \\\n | awk '{ print length, $0 }' \\\n | sort -n \\\n | head -n1 \\\n | cut -d' ' -f2-\n )\n if [ -n \"$root_app_dir\" ]; then\n echo \"sudo rmdir -p \\\"$root_app_dir\\\" 2>/dev/null || :\"\n sudo rmdir -p \"$root_app_dir\" 2>/dev/null || :\n fi\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.crashplan.engine'\nquit_application 'com.crashplan.app'\n(cd /Users/$LOGGED_IN_USER && sudo 'Uninstall.app/Contents/Resources/uninstall.sh')\nremove_pkg_files 'com.crashplan.app.pkg'\nforget_pkg 'com.crashplan.app.pkg'\nremove_pkg_files 'com.crashplan.uninstaller.pkg'\nforget_pkg 'com.crashplan.uninstaller.pkg'\ntrash $LOGGED_IN_USER '/Library/Application Support/CrashPlan'\ntrash $LOGGED_IN_USER '/Library/Caches/CrashPlan'\ntrash $LOGGED_IN_USER '/Library/LaunchDaemons/com.crashplan.service.plist'\ntrash $LOGGED_IN_USER '/Library/Logs/CrashPlan'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.crashplan.desktop.sfl*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/CrashPlan'\ntrash $LOGGED_IN_USER '~/Library/LaunchAgents/com.crashplan.menubar.plist'\ntrash $LOGGED_IN_USER '~/Library/Logs/CrashPlan'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.crashplan.desktop.plist'\n",
"392300f9": "#!/bin/bash\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nexpand_pkgid_and_map() {\n local PKGID=\"$1\"\n local FUNC=\"$2\"\n if [[ \"$PKGID\" == *\"*\" ]]; then\n local prefix=\"${PKGID%\\*}\"\n echo \"Expanding wildcard for PKGID: $PKGID\"\n for receipt in $(pkgutil --pkgs | grep \"^${prefix}\"); do\n echo \"Processing $receipt\"\n \"$FUNC\" \"$receipt\"\n done\n else\n \"$FUNC\" \"$PKGID\"\n fi\n}\n\nforget_pkg() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" forget_receipt\n}\n\nforget_receipt() {\n local PKGID=\"$1\"\n sudo pkgutil --forget \"$PKGID\"\n}\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n # A wildcard label can't be used with launchctl or as a plist name, so expand\n # it to the labels of currently loaded services that match the pattern.\n local services=(\"$service\")\n if [[ \"$service\" == *\"*\"* ]]; then\n local regex\n # Escape regex metacharacters, turn '*' into '.*', and anchor the pattern so\n # it matches a full label rather than a substring.\n regex=$(printf '%s' \"$service\" | sed -e 's/[][(){}.^$+?|\\\\]/\\\\&/g' -e 's/\\*/.*/g')\n regex=\"^${regex}$\"\n services=()\n local id\n # Match every loaded job by label regardless of PID; launchctl list reports\n # loaded-but-not-running jobs with a \"-\" in the PID column.\n while read -r _ _ id; do\n [[ \"$id\" =~ $regex ]] && services+=(\"$id\")\n done < <(launchctl list 2>/dev/null | tail -n +2)\n if [[ ${#services[@]} -eq 0 ]]; then\n echo \"No loaded launchctl service matches ${service}\"\n return\n fi\n fi\n\n local service_label\n for service_label in \"${services[@]}\"; do\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service_label}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service_label}\"\n else\n launchctl remove \"${service_label}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service_label}.plist\"\n \"/Library/LaunchDaemons/${service_label}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n done\n}\n\nremove_pkg_files() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" remove_receipt_files\n}\n\nremove_receipt_files() {\n local PKGID=\"$1\"\n local PKGINFO VOLUME INSTALL_LOCATION FULL_INSTALL_LOCATION\n\n echo \"pkgutil --pkg-info-plist \\\"$PKGID\\\"\"\n PKGINFO=$(pkgutil --pkg-info-plist \"$PKGID\")\n VOLUME=$(echo \"$PKGINFO\" | awk '/<key>volume<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n INSTALL_LOCATION=$(echo \"$PKGINFO\" | awk '/<key>install-location<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n\n if [ -z \"$INSTALL_LOCATION\" ] || [ \"$INSTALL_LOCATION\" = \"/\" ]; then\n FULL_INSTALL_LOCATION=\"$VOLUME\"\n else\n FULL_INSTALL_LOCATION=\"$VOLUME/$INSTALL_LOCATION\"\n FULL_INSTALL_LOCATION=$(echo \"$FULL_INSTALL_LOCATION\" | sed 's|//|/|g')\n fi\n\n echo \"sudo pkgutil --only-files --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-files --files \"$PKGID\" | sed \"s|^|/${INSTALL_LOCATION}/|\" | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n echo \"sudo pkgutil --only-dirs --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | grep '\\\\.app$' | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-dirs --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | grep '\\.app$' | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n root_app_dir=$(\n sudo pkgutil --only-dirs --files \"$PKGID\" \\\n | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" \\\n | grep 'Applications' \\\n | awk '{ print length, $0 }' \\\n | sort -n \\\n | head -n1 \\\n | cut -d' ' -f2-\n )\n if [ -n \"$root_app_dir\" ]; then\n echo \"sudo rmdir -p \\\"$root_app_dir\\\" 2>/dev/null || :\"\n sudo rmdir -p \"$root_app_dir\" 2>/dev/null || :\n fi\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.crashplan.engine'\nquit_application 'com.crashplan.app'\n(cd /Users/$LOGGED_IN_USER && sudo 'Uninstall.app/Contents/Resources/uninstall.sh')\nremove_pkg_files 'com.crashplan.app.pkg'\nforget_pkg 'com.crashplan.app.pkg'\nremove_pkg_files 'com.crashplan.uninstaller.pkg'\nforget_pkg 'com.crashplan.uninstaller.pkg'\ntrash $LOGGED_IN_USER '/Library/Application Support/CrashPlan'\ntrash $LOGGED_IN_USER '/Library/Caches/CrashPlan'\ntrash $LOGGED_IN_USER '/Library/LaunchDaemons/com.crashplan.service.plist'\ntrash $LOGGED_IN_USER '/Library/Logs/CrashPlan'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.crashplan.desktop.sfl*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/CrashPlan'\ntrash $LOGGED_IN_USER '~/Library/LaunchAgents/com.crashplan.menubar.plist'\ntrash $LOGGED_IN_USER '~/Library/Logs/CrashPlan'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.crashplan.desktop.plist'\n",

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Expand wildcard labels from plist files before returning.

The wildcard branch populates services only from currently loaded launchctl list labels and returns when none are loaded. This skips deleting matching LaunchAgent/LaunchDaemon plist files for installed-but-unloaded jobs, making wildcard uninstall a no-op in that state.

Proposed Bash helper adjustment
     services=()
     local id
     # Match every loaded job by label regardless of PID; launchctl list reports
     # loaded-but-not-running jobs with a "-" in the PID column.
     while read -r _ _ id; do
       [[ "$id" =~ $regex ]] && services+=("$id")
     done < <(launchctl list 2>/dev/null | tail -n +2)
+
+    local dir plist
+    for dir in "/Library/LaunchAgents" "/Library/LaunchDaemons" "${HOME}/Library/LaunchAgents" "${HOME}/Library/LaunchDaemons"; do
+      [[ -d "$dir" ]] || continue
+      while IFS= read -r -d '' plist; do
+        id="$(basename "$plist" .plist)"
+        [[ "$id" =~ $regex ]] && services+=("$id")
+      done < <(find "$dir" -maxdepth 1 -name '*.plist' -print0 2>/dev/null)
+    done
+
     if [[ ${`#services`[@]} -eq 0 ]]; then
       echo "No loaded launchctl service matches ${service}"
       return
     fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"392300f9": "#!/bin/bash\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nexpand_pkgid_and_map() {\n local PKGID=\"$1\"\n local FUNC=\"$2\"\n if [[ \"$PKGID\" == *\"*\" ]]; then\n local prefix=\"${PKGID%\\*}\"\n echo \"Expanding wildcard for PKGID: $PKGID\"\n for receipt in $(pkgutil --pkgs | grep \"^${prefix}\"); do\n echo \"Processing $receipt\"\n \"$FUNC\" \"$receipt\"\n done\n else\n \"$FUNC\" \"$PKGID\"\n fi\n}\n\nforget_pkg() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" forget_receipt\n}\n\nforget_receipt() {\n local PKGID=\"$1\"\n sudo pkgutil --forget \"$PKGID\"\n}\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n # A wildcard label can't be used with launchctl or as a plist name, so expand\n # it to the labels of currently loaded services that match the pattern.\n local services=(\"$service\")\n if [[ \"$service\" == *\"*\"* ]]; then\n local regex\n # Escape regex metacharacters, turn '*' into '.*', and anchor the pattern so\n # it matches a full label rather than a substring.\n regex=$(printf '%s' \"$service\" | sed -e 's/[][(){}.^$+?|\\\\]/\\\\&/g' -e 's/\\*/.*/g')\n regex=\"^${regex}$\"\n services=()\n local id\n # Match every loaded job by label regardless of PID; launchctl list reports\n # loaded-but-not-running jobs with a \"-\" in the PID column.\n while read -r _ _ id; do\n [[ \"$id\" =~ $regex ]] && services+=(\"$id\")\n done < <(launchctl list 2>/dev/null | tail -n +2)\n if [[ ${#services[@]} -eq 0 ]]; then\n echo \"No loaded launchctl service matches ${service}\"\n return\n fi\n fi\n\n local service_label\n for service_label in \"${services[@]}\"; do\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service_label}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service_label}\"\n else\n launchctl remove \"${service_label}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service_label}.plist\"\n \"/Library/LaunchDaemons/${service_label}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n done\n}\n\nremove_pkg_files() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" remove_receipt_files\n}\n\nremove_receipt_files() {\n local PKGID=\"$1\"\n local PKGINFO VOLUME INSTALL_LOCATION FULL_INSTALL_LOCATION\n\n echo \"pkgutil --pkg-info-plist \\\"$PKGID\\\"\"\n PKGINFO=$(pkgutil --pkg-info-plist \"$PKGID\")\n VOLUME=$(echo \"$PKGINFO\" | awk '/<key>volume<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n INSTALL_LOCATION=$(echo \"$PKGINFO\" | awk '/<key>install-location<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n\n if [ -z \"$INSTALL_LOCATION\" ] || [ \"$INSTALL_LOCATION\" = \"/\" ]; then\n FULL_INSTALL_LOCATION=\"$VOLUME\"\n else\n FULL_INSTALL_LOCATION=\"$VOLUME/$INSTALL_LOCATION\"\n FULL_INSTALL_LOCATION=$(echo \"$FULL_INSTALL_LOCATION\" | sed 's|//|/|g')\n fi\n\n echo \"sudo pkgutil --only-files --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-files --files \"$PKGID\" | sed \"s|^|/${INSTALL_LOCATION}/|\" | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n echo \"sudo pkgutil --only-dirs --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | grep '\\\\.app$' | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-dirs --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | grep '\\.app$' | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n root_app_dir=$(\n sudo pkgutil --only-dirs --files \"$PKGID\" \\\n | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" \\\n | grep 'Applications' \\\n | awk '{ print length, $0 }' \\\n | sort -n \\\n | head -n1 \\\n | cut -d' ' -f2-\n )\n if [ -n \"$root_app_dir\" ]; then\n echo \"sudo rmdir -p \\\"$root_app_dir\\\" 2>/dev/null || :\"\n sudo rmdir -p \"$root_app_dir\" 2>/dev/null || :\n fi\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.crashplan.engine'\nquit_application 'com.crashplan.app'\n(cd /Users/$LOGGED_IN_USER && sudo 'Uninstall.app/Contents/Resources/uninstall.sh')\nremove_pkg_files 'com.crashplan.app.pkg'\nforget_pkg 'com.crashplan.app.pkg'\nremove_pkg_files 'com.crashplan.uninstaller.pkg'\nforget_pkg 'com.crashplan.uninstaller.pkg'\ntrash $LOGGED_IN_USER '/Library/Application Support/CrashPlan'\ntrash $LOGGED_IN_USER '/Library/Caches/CrashPlan'\ntrash $LOGGED_IN_USER '/Library/LaunchDaemons/com.crashplan.service.plist'\ntrash $LOGGED_IN_USER '/Library/Logs/CrashPlan'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.crashplan.desktop.sfl*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/CrashPlan'\ntrash $LOGGED_IN_USER '~/Library/LaunchAgents/com.crashplan.menubar.plist'\ntrash $LOGGED_IN_USER '~/Library/Logs/CrashPlan'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.crashplan.desktop.plist'\n",
services=()
local id
# Match every loaded job by label regardless of PID; launchctl list reports
# loaded-but-not-running jobs with a "-" in the PID column.
while read -r _ _ id; do
[[ "$id" =~ $regex ]] && services+=("$id")
done < <(launchctl list 2>/dev/null | tail -n +2)
local dir plist
for dir in "/Library/LaunchAgents" "/Library/LaunchDaemons" "${HOME}/Library/LaunchAgents" "${HOME}/Library/LaunchDaemons"; do
[[ -d "$dir" ]] || continue
while IFS= read -r -d '' plist; do
id="$(basename "$plist" .plist)"
[[ "$id" =~ $regex ]] && services+=("$id")
done < <(find "$dir" -maxdepth 1 -name '*.plist' -print0 2>/dev/null)
done
if [[ ${`#services`[@]} -eq 0 ]]; then
echo "No loaded launchctl service matches ${service}"
return
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/maintained-apps/outputs/crashplan/darwin.json` at line 19, The wildcard
handling in remove_launchctl_service only expands against loaded launchctl
labels, so unloaded but installed jobs are missed. Update the wildcard branch to
also scan the LaunchAgents/LaunchDaemons plist paths for matching labels before
returning, and merge those matches into services so the cleanup loop can delete
their plist files even when launchctl list has no active entry.

"refs": {
"917a2397": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath \"$INSTALLER_PATH\")\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Launch the app in the logged-in user's GUI session. Apps launched by root\n # won't register with the user's Dock/GUI, so run 'open' as the console user.\n # Use 'launchctl asuser' to bootstrap into the console user's Mach namespace\n # and GUI session — 'sudo -u' alone doesn't do this, which can cause\n # LSOpenURLsWithRole() failures even when 'open' exits 0.\n local open_status=0\n if [[ $EUID -eq 0 ]]; then\n local console_uid\n console_uid=$(id -u \"$console_user\")\n /bin/launchctl asuser \"$console_uid\" sudo -u \"$console_user\" open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n else\n open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n fi\n\n if [[ $open_status -eq 0 ]]; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# extract contents\nMOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)\nyes | hdiutil attach -plist -nobrowse -readonly -mountpoint \"$MOUNT_POINT\" \"$INSTALLER_PATH\" || exit 1\nsudo cp -R \"$MOUNT_POINT\"/* \"$TMPDIR\"\nhdiutil detach \"$MOUNT_POINT\" || true\n# copy to the applications folder\nquit_and_track_application 'com.exafunction.windsurf'\nif [ -d \"$APPDIR/Devin.app\" ]; then\n\tsudo mv \"$APPDIR/Devin.app\" \"$TMPDIR/Devin.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Devin.app\" \"$APPDIR\"\nrelaunch_application 'com.exafunction.windsurf'\n",
"cb4ce7dc": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service}\"\n else\n launchctl remove \"${service}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service}.plist\"\n \"/Library/LaunchDaemons/${service}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.exafunction.windsurf.ShipIt'\nquit_application 'com.exafunction.windsurf'\nsudo rm -rf \"$APPDIR/Devin.app\"\nsudo rmdir '~/.codeium/windsurf'\ntrash $LOGGED_IN_USER '~/.devin'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.exafunction.windsurf.sfl*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Devin'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.exafunction.windsurf'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.exafunction.windsurf.ShipIt'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.exafunction.windsurf'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.exafunction.windsurf.plist'\n"
"4fdaa2d1": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n # A wildcard label can't be used with launchctl or as a plist name, so expand\n # it to the labels of currently loaded services that match the pattern.\n local services=(\"$service\")\n if [[ \"$service\" == *\"*\"* ]]; then\n local regex\n # Escape regex metacharacters, turn '*' into '.*', and anchor the pattern so\n # it matches a full label rather than a substring.\n regex=$(printf '%s' \"$service\" | sed -e 's/[][(){}.^$+?|\\\\]/\\\\&/g' -e 's/\\*/.*/g')\n regex=\"^${regex}$\"\n services=()\n local id\n # Match every loaded job by label regardless of PID; launchctl list reports\n # loaded-but-not-running jobs with a \"-\" in the PID column.\n while read -r _ _ id; do\n [[ \"$id\" =~ $regex ]] && services+=(\"$id\")\n done < <(launchctl list 2>/dev/null | tail -n +2)\n if [[ ${#services[@]} -eq 0 ]]; then\n echo \"No loaded launchctl service matches ${service}\"\n return\n fi\n fi\n\n local service_label\n for service_label in \"${services[@]}\"; do\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service_label}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service_label}\"\n else\n launchctl remove \"${service_label}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service_label}.plist\"\n \"/Library/LaunchDaemons/${service_label}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.exafunction.windsurf.ShipIt'\nquit_application 'com.exafunction.windsurf'\nsudo rm -rf \"$APPDIR/Devin.app\"\nsudo rmdir '~/.codeium/windsurf'\ntrash $LOGGED_IN_USER '~/.devin'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.exafunction.windsurf.sfl*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Devin'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.exafunction.windsurf'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.exafunction.windsurf.ShipIt'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.exafunction.windsurf'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.exafunction.windsurf.plist'\n",

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Remove the logged-in user’s path instead of a literal ~ path.

sudo rmdir '~/.codeium/windsurf' keeps ~ literal, so the intended /Users/$LOGGED_IN_USER/.codeium/windsurf directory is left behind.

Proposed fix
-sudo rmdir '~/.codeium/windsurf'
+sudo rmdir "/Users/${LOGGED_IN_USER}/.codeium/windsurf" 2>/dev/null || :
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"4fdaa2d1": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n # A wildcard label can't be used with launchctl or as a plist name, so expand\n # it to the labels of currently loaded services that match the pattern.\n local services=(\"$service\")\n if [[ \"$service\" == *\"*\"* ]]; then\n local regex\n # Escape regex metacharacters, turn '*' into '.*', and anchor the pattern so\n # it matches a full label rather than a substring.\n regex=$(printf '%s' \"$service\" | sed -e 's/[][(){}.^$+?|\\\\]/\\\\&/g' -e 's/\\*/.*/g')\n regex=\"^${regex}$\"\n services=()\n local id\n # Match every loaded job by label regardless of PID; launchctl list reports\n # loaded-but-not-running jobs with a \"-\" in the PID column.\n while read -r _ _ id; do\n [[ \"$id\" =~ $regex ]] && services+=(\"$id\")\n done < <(launchctl list 2>/dev/null | tail -n +2)\n if [[ ${#services[@]} -eq 0 ]]; then\n echo \"No loaded launchctl service matches ${service}\"\n return\n fi\n fi\n\n local service_label\n for service_label in \"${services[@]}\"; do\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service_label}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service_label}\"\n else\n launchctl remove \"${service_label}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service_label}.plist\"\n \"/Library/LaunchDaemons/${service_label}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.exafunction.windsurf.ShipIt'\nquit_application 'com.exafunction.windsurf'\nsudo rm -rf \"$APPDIR/Devin.app\"\nsudo rmdir '~/.codeium/windsurf'\ntrash $LOGGED_IN_USER '~/.devin'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.exafunction.windsurf.sfl*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Devin'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.exafunction.windsurf'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.exafunction.windsurf.ShipIt'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.exafunction.windsurf'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.exafunction.windsurf.plist'\n",
"4fdaa2d1": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n # A wildcard label can't be used with launchctl or as a plist name, so expand\n # it to the labels of currently loaded services that match the pattern.\n local services=(\"$service\")\n if [[ \"$service\" == *\"*\"* ]]; then\n local regex\n # Escape regex metacharacters, turn '*' into '.*', and anchor the pattern so\n # it matches a full label rather than a substring.\n regex=$(printf '%s' \"$service\" | sed -e 's/[][(){}.^$+?|\\\\]/\\\\&/g' -e 's/\\*/.*/g')\n regex=\"^${regex}$\"\n services=()\n local id\n # Match every loaded job by label regardless of PID; launchctl list reports\n # loaded-but-not-running jobs with a \"-\" in the PID column.\n while read -r _ _ id; do\n [[ \"$id\" =~ $regex ]] && services+=(\"$id\")\n done < <(launchctl list 2>/dev/null | tail -n +2)\n if [[ ${`#services`[@]} -eq 0 ]]; then\n echo \"No loaded launchctl service matches ${service}\"\n return\n fi\n fi\n\n local service_label\n for service_label in \"${services[@]}\"; do\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service_label}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service_label}\"\n else\n launchctl remove \"${service_label}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service_label}.plist\"\n \"/Library/LaunchDaemons/${service_label}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.exafunction.windsurf.ShipIt'\nquit_application 'com.exafunction.windsurf'\nsudo rm -rf \"$APPDIR/Devin.app\"\nsudo rmdir \"/Users/${LOGGED_IN_USER}/.codeium/windsurf\" 2>/dev/null || :\ntrash $LOGGED_IN_USER '~/.devin'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments/com.exafunction.windsurf.sfl*'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Devin'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.exafunction.windsurf'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.exafunction.windsurf.ShipIt'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.exafunction.windsurf'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.exafunction.windsurf.plist'\n",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/maintained-apps/outputs/devin-desktop/darwin.json` at line 19, The cleanup
step in the shell script is using a quoted tilde path, so `sudo rmdir
'~/.codeium/windsurf'` will not expand to the logged-in user’s home directory
and leaves the directory behind. Update the removal logic near the `APPDIR`,
`LOGGED_IN_USER`, and `trash` usage to target the resolved user path instead of
a literal `~`, using the same user-derived path style already used elsewhere in
the script. Keep the fix scoped to the `rmdir` invocation so the uninstall flow
removes the intended directory.

"50188cf7": "#!/bin/bash\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service}\"\n else\n launchctl remove \"${service}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service}.plist\"\n \"/Library/LaunchDaemons/${service}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.express.vpn.client'\nremove_launchctl_service 'com.express.vpn.daemon'\nremove_launchctl_service 'com.express.vpn.installhelper'\nquit_application 'com.express.vpn'\nsudo rm -rf '/Applications/ExpressVPN.app'\ntrash $LOGGED_IN_USER '/Library/Application Support/com.express.vpn'\ntrash $LOGGED_IN_USER '/Library/Preferences/com.express.vpn'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.express.vpn'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.express.vpn'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.express.vpn'\n",
"c5afbbfa": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(mktemp -d)\n\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n if ! osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n# extract contents\nunzip \"$INSTALLER_PATH\" -d \"$TMPDIR\"\n\n# discover the installer app by finding any .app that contains an installer executable\nINSTALLER_APP=\"\"\nfor app in \"$TMPDIR\"/*.app; do\n if [ -d \"$app\" ] && [ -d \"$app/Contents/MacOS\" ]; then\n INSTALLER_APP=\"$app\"\n break\n fi\ndone\n\nif [ -z \"$INSTALLER_APP\" ] || [ ! -d \"$INSTALLER_APP\" ]; then\n echo \"Error: Installer app not found in $TMPDIR\"\n exit 1\nfi\n\nquit_application 'com.expressvpn.ExpressVPN'\n\n# Remove quarantine attributes so Gatekeeper won't block binaries during install\nsudo xattr -r -d com.apple.quarantine \"$INSTALLER_APP\" 2>/dev/null || true\n\n# Run the bundled installer script which handles copying to /Applications,\n# setting permissions, creating groups, installing the LaunchDaemon, and\n# starting the daemon\nINSTALLER_SCRIPT=\"$INSTALLER_APP/Contents/Resources/vpn-installer.sh\"\nif [ ! -f \"$INSTALLER_SCRIPT\" ]; then\n echo \"Error: vpn-installer.sh not found in $INSTALLER_APP/Contents/Resources\"\n exit 1\nfi\n\nchmod +x \"$INSTALLER_SCRIPT\"\nsudo bash \"$INSTALLER_SCRIPT\"\nEXIT_CODE=$?\n\nif [ $EXIT_CODE -ne 0 ]; then\n echo \"Error: Installer exited with code $EXIT_CODE\"\n exit $EXIT_CODE\nfi\n\n"
"c5afbbfa": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(mktemp -d)\n\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n if ! osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n# extract contents\nunzip \"$INSTALLER_PATH\" -d \"$TMPDIR\"\n\n# discover the installer app by finding any .app that contains an installer executable\nINSTALLER_APP=\"\"\nfor app in \"$TMPDIR\"/*.app; do\n if [ -d \"$app\" ] && [ -d \"$app/Contents/MacOS\" ]; then\n INSTALLER_APP=\"$app\"\n break\n fi\ndone\n\nif [ -z \"$INSTALLER_APP\" ] || [ ! -d \"$INSTALLER_APP\" ]; then\n echo \"Error: Installer app not found in $TMPDIR\"\n exit 1\nfi\n\nquit_application 'com.expressvpn.ExpressVPN'\n\n# Remove quarantine attributes so Gatekeeper won't block binaries during install\nsudo xattr -r -d com.apple.quarantine \"$INSTALLER_APP\" 2>/dev/null || true\n\n# Run the bundled installer script which handles copying to /Applications,\n# setting permissions, creating groups, installing the LaunchDaemon, and\n# starting the daemon\nINSTALLER_SCRIPT=\"$INSTALLER_APP/Contents/Resources/vpn-installer.sh\"\nif [ ! -f \"$INSTALLER_SCRIPT\" ]; then\n echo \"Error: vpn-installer.sh not found in $INSTALLER_APP/Contents/Resources\"\n exit 1\nfi\n\nchmod +x \"$INSTALLER_SCRIPT\"\nsudo bash \"$INSTALLER_SCRIPT\"\nEXIT_CODE=$?\n\nif [ $EXIT_CODE -ne 0 ]; then\n echo \"Error: Installer exited with code $EXIT_CODE\"\n exit $EXIT_CODE\nfi\n\n",
"f6060d51": "#!/bin/bash\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n # A wildcard label can't be used with launchctl or as a plist name, so expand\n # it to the labels of currently loaded services that match the pattern.\n local services=(\"$service\")\n if [[ \"$service\" == *\"*\"* ]]; then\n local regex\n # Escape regex metacharacters, turn '*' into '.*', and anchor the pattern so\n # it matches a full label rather than a substring.\n regex=$(printf '%s' \"$service\" | sed -e 's/[][(){}.^$+?|\\\\]/\\\\&/g' -e 's/\\*/.*/g')\n regex=\"^${regex}$\"\n services=()\n local id\n # Match every loaded job by label regardless of PID; launchctl list reports\n # loaded-but-not-running jobs with a \"-\" in the PID column.\n while read -r _ _ id; do\n [[ \"$id\" =~ $regex ]] && services+=(\"$id\")\n done < <(launchctl list 2>/dev/null | tail -n +2)\n if [[ ${#services[@]} -eq 0 ]]; then\n echo \"No loaded launchctl service matches ${service}\"\n return\n fi\n fi\n\n local service_label\n for service_label in \"${services[@]}\"; do\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service_label}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service_label}\"\n else\n launchctl remove \"${service_label}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service_label}.plist\"\n \"/Library/LaunchDaemons/${service_label}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.express.vpn.client'\nremove_launchctl_service 'com.express.vpn.daemon'\nremove_launchctl_service 'com.express.vpn.installhelper'\nquit_application 'com.express.vpn'\nsudo rm -rf '/Applications/ExpressVPN.app'\ntrash $LOGGED_IN_USER '/Library/Application Support/com.express.vpn'\ntrash $LOGGED_IN_USER '/Library/Preferences/com.express.vpn'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.express.vpn'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.express.vpn'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.express.vpn'\n"

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the app’s actual bundle identifier when quitting ExpressVPN.

Line 20 calls quit_application 'com.express.vpn', but Line 6 and the install script use com.expressvpn.ExpressVPN. The uninstall may remove the app bundle while the GUI app is still running.

🐛 Proposed fix
-quit_application 'com.express.vpn'
+quit_application 'com.expressvpn.ExpressVPN'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"f6060d51": "#!/bin/bash\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n # A wildcard label can't be used with launchctl or as a plist name, so expand\n # it to the labels of currently loaded services that match the pattern.\n local services=(\"$service\")\n if [[ \"$service\" == *\"*\"* ]]; then\n local regex\n # Escape regex metacharacters, turn '*' into '.*', and anchor the pattern so\n # it matches a full label rather than a substring.\n regex=$(printf '%s' \"$service\" | sed -e 's/[][(){}.^$+?|\\\\]/\\\\&/g' -e 's/\\*/.*/g')\n regex=\"^${regex}$\"\n services=()\n local id\n # Match every loaded job by label regardless of PID; launchctl list reports\n # loaded-but-not-running jobs with a \"-\" in the PID column.\n while read -r _ _ id; do\n [[ \"$id\" =~ $regex ]] && services+=(\"$id\")\n done < <(launchctl list 2>/dev/null | tail -n +2)\n if [[ ${#services[@]} -eq 0 ]]; then\n echo \"No loaded launchctl service matches ${service}\"\n return\n fi\n fi\n\n local service_label\n for service_label in \"${services[@]}\"; do\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service_label}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service_label}\"\n else\n launchctl remove \"${service_label}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service_label}.plist\"\n \"/Library/LaunchDaemons/${service_label}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.express.vpn.client'\nremove_launchctl_service 'com.express.vpn.daemon'\nremove_launchctl_service 'com.express.vpn.installhelper'\nquit_application 'com.express.vpn'\nsudo rm -rf '/Applications/ExpressVPN.app'\ntrash $LOGGED_IN_USER '/Library/Application Support/com.express.vpn'\ntrash $LOGGED_IN_USER '/Library/Preferences/com.express.vpn'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.express.vpn'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.express.vpn'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.express.vpn'\n"
remove_launchctl_service 'com.express.vpn.client'
remove_launchctl_service 'com.express.vpn.daemon'
remove_launchctl_service 'com.express.vpn.installhelper'
quit_application 'com.expressvpn.ExpressVPN'
sudo rm -rf '/Applications/ExpressVPN.app'
trash $LOGGED_IN_USER '/Library/Application Support/com.express.vpn'
trash $LOGGED_IN_USER '/Library/Preferences/com.express.vpn'
trash $LOGGED_IN_USER '~/Library/Application Support/com.express.vpn'
trash $LOGGED_IN_USER '~/Library/Caches/com.express.vpn'
trash $LOGGED_IN_USER '~/Library/Preferences/com.express.vpn'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/maintained-apps/outputs/expressvpn/darwin.json` at line 20, The uninstall
script is quitting ExpressVPN with the wrong bundle identifier, so the running
GUI app may be missed before removal. Update the call to quit_application in the
uninstall flow to use the same actual bundle ID used elsewhere in the script and
install logic, com.expressvpn.ExpressVPN, so the app is correctly detected and
closed before deleting the bundle.

"refs": {
"4ef03933": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nTMPDIR=$(dirname \"$(realpath \"$INSTALLER_PATH\")\")\n# functions\n\nquit_and_track_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n eval \"export $var_name=0\"\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n eval \"export $var_name=0\"\n return\n fi\n\n # App was running, mark it for relaunch\n eval \"export $var_name=1\"\n echo \"Application '$bundle_id' was running; will relaunch after installation.\"\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nrelaunch_application() {\n local bundle_id=\"$1\"\n local var_name=\"APP_WAS_RUNNING_$(echo \"$bundle_id\" | tr '.-' '__')\"\n local was_running\n\n # Check if the app was running before installation\n eval \"was_running=\\$$var_name\"\n if [[ \"$was_running\" != \"1\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping relaunching application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Relaunching application '$bundle_id'...\"\n\n # Launch the app in the logged-in user's GUI session. Apps launched by root\n # won't register with the user's Dock/GUI, so run 'open' as the console user.\n # Use 'launchctl asuser' to bootstrap into the console user's Mach namespace\n # and GUI session — 'sudo -u' alone doesn't do this, which can cause\n # LSOpenURLsWithRole() failures even when 'open' exits 0.\n local open_status=0\n if [[ $EUID -eq 0 ]]; then\n local console_uid\n console_uid=$(id -u \"$console_user\")\n /bin/launchctl asuser \"$console_uid\" sudo -u \"$console_user\" open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n else\n open -b \"$bundle_id\" >/dev/null 2>&1 || open_status=$?\n fi\n\n if [[ $open_status -eq 0 ]]; then\n echo \"Application '$bundle_id' relaunched successfully.\"\n else\n echo \"Failed to relaunch application '$bundle_id'.\"\n fi\n}\n\n\n# extract contents\nMOUNT_POINT=$(mktemp -d /tmp/dmg_mount_XXXXXX)\nyes | hdiutil attach -plist -nobrowse -readonly -mountpoint \"$MOUNT_POINT\" \"$INSTALLER_PATH\" || exit 1\nsudo cp -R \"$MOUNT_POINT\"/* \"$TMPDIR\"\nhdiutil detach \"$MOUNT_POINT\" || true\n# copy to the applications folder\nquit_and_track_application 'com.mschrage.fig'\nif [ -d \"$APPDIR/Fig.app\" ]; then\n\tsudo mv \"$APPDIR/Fig.app\" \"$TMPDIR/Fig.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Fig.app\" \"$APPDIR\"\nrelaunch_application 'com.mschrage.fig'\nmkdir -p .\n/bin/ln -h -f -s -- \"$APPDIR/Fig.app/Contents/MacOS/fig-darwin-universal\" \"fig\"\n",
"7c764ab7": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service}\"\n else\n launchctl remove \"${service}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service}.plist\"\n \"/Library/LaunchDaemons/${service}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'io.fig.dotfiles-daemon'\nremove_launchctl_service 'io.fig.launcher'\nremove_launchctl_service 'io.fig.uninstall'\nquit_application 'com.mschrage.fig'\nquit_application 'io.fig.cursor'\n(cd /Users/$LOGGED_IN_USER && '$APPDIR/Fig.app/Contents/MacOS/fig-darwin-universal' '_' 'brew-uninstall')\nsudo rm -rf \"$APPDIR/Fig.app\"\nsudo rm -rf 'fig'\ntrash $LOGGED_IN_USER '~/.fig'\ntrash $LOGGED_IN_USER '~/.fig.dotfiles.bak'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.mschrage.fig'\ntrash $LOGGED_IN_USER '~/Library/Application Support/fig'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.mschrage.fig'\ntrash $LOGGED_IN_USER '~/Library/Caches/fig'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.mschrage.fig'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.mschrage.fig.*'\ntrash $LOGGED_IN_USER '~/Library/WebKit/com.mschrage.fig'\n"
"bb2cbed3": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n # A wildcard label can't be used with launchctl or as a plist name, so expand\n # it to the labels of currently loaded services that match the pattern.\n local services=(\"$service\")\n if [[ \"$service\" == *\"*\"* ]]; then\n local regex\n # Escape regex metacharacters, turn '*' into '.*', and anchor the pattern so\n # it matches a full label rather than a substring.\n regex=$(printf '%s' \"$service\" | sed -e 's/[][(){}.^$+?|\\\\]/\\\\&/g' -e 's/\\*/.*/g')\n regex=\"^${regex}$\"\n services=()\n local id\n # Match every loaded job by label regardless of PID; launchctl list reports\n # loaded-but-not-running jobs with a \"-\" in the PID column.\n while read -r _ _ id; do\n [[ \"$id\" =~ $regex ]] && services+=(\"$id\")\n done < <(launchctl list 2>/dev/null | tail -n +2)\n if [[ ${#services[@]} -eq 0 ]]; then\n echo \"No loaded launchctl service matches ${service}\"\n return\n fi\n fi\n\n local service_label\n for service_label in \"${services[@]}\"; do\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service_label}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service_label}\"\n else\n launchctl remove \"${service_label}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service_label}.plist\"\n \"/Library/LaunchDaemons/${service_label}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'io.fig.dotfiles-daemon'\nremove_launchctl_service 'io.fig.launcher'\nremove_launchctl_service 'io.fig.uninstall'\nquit_application 'com.mschrage.fig'\nquit_application 'io.fig.cursor'\n(cd /Users/$LOGGED_IN_USER && '$APPDIR/Fig.app/Contents/MacOS/fig-darwin-universal' '_' 'brew-uninstall')\nsudo rm -rf \"$APPDIR/Fig.app\"\nsudo rm -rf 'fig'\ntrash $LOGGED_IN_USER '~/.fig'\ntrash $LOGGED_IN_USER '~/.fig.dotfiles.bak'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.mschrage.fig'\ntrash $LOGGED_IN_USER '~/Library/Application Support/fig'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.mschrage.fig'\ntrash $LOGGED_IN_USER '~/Library/Caches/fig'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.mschrage.fig'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.mschrage.fig.*'\ntrash $LOGGED_IN_USER '~/Library/WebKit/com.mschrage.fig'\n"

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the embedded uninstall script no longer contains the literal '$APPDIR' executable path.
jq -r '.refs.bb2cbed3' ee/maintained-apps/outputs/fig/darwin.json | grep -n "brew-uninstall"

Repository: fleetdm/fleet

Length of output: 261


🏁 Script executed:

python3 - <<'PY'
import subprocess, textwrap
script = textwrap.dedent(r'''
APPDIR="/Applications/"
LOGGED_IN_USER="user"
set -x
(cd "/Users/$LOGGED_IN_USER" && '$APPDIR/Fig.app/Contents/MacOS/fig-darwin-universal' '_' 'brew-uninstall')
''')
p = subprocess.run(["bash", "-lc", script], capture_output=True, text=True)
print("returncode:", p.returncode)
print("stdout:")
print(p.stdout)
print("stderr:")
print(p.stderr)
PY

Repository: fleetdm/fleet

Length of output: 356


Expand $APPDIR before the uninstall hook call.
The single-quoted executable path keeps $APPDIR literal, so the brew-uninstall hook is never invoked.

Proposed fix
-(cd /Users/$LOGGED_IN_USER && '$APPDIR/Fig.app/Contents/MacOS/fig-darwin-universal' '_' 'brew-uninstall')
+(cd "/Users/$LOGGED_IN_USER" && "${APPDIR}Fig.app/Contents/MacOS/fig-darwin-universal" '_' 'brew-uninstall')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"bb2cbed3": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n # A wildcard label can't be used with launchctl or as a plist name, so expand\n # it to the labels of currently loaded services that match the pattern.\n local services=(\"$service\")\n if [[ \"$service\" == *\"*\"* ]]; then\n local regex\n # Escape regex metacharacters, turn '*' into '.*', and anchor the pattern so\n # it matches a full label rather than a substring.\n regex=$(printf '%s' \"$service\" | sed -e 's/[][(){}.^$+?|\\\\]/\\\\&/g' -e 's/\\*/.*/g')\n regex=\"^${regex}$\"\n services=()\n local id\n # Match every loaded job by label regardless of PID; launchctl list reports\n # loaded-but-not-running jobs with a \"-\" in the PID column.\n while read -r _ _ id; do\n [[ \"$id\" =~ $regex ]] && services+=(\"$id\")\n done < <(launchctl list 2>/dev/null | tail -n +2)\n if [[ ${#services[@]} -eq 0 ]]; then\n echo \"No loaded launchctl service matches ${service}\"\n return\n fi\n fi\n\n local service_label\n for service_label in \"${services[@]}\"; do\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service_label}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service_label}\"\n else\n launchctl remove \"${service_label}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service_label}.plist\"\n \"/Library/LaunchDaemons/${service_label}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'io.fig.dotfiles-daemon'\nremove_launchctl_service 'io.fig.launcher'\nremove_launchctl_service 'io.fig.uninstall'\nquit_application 'com.mschrage.fig'\nquit_application 'io.fig.cursor'\n(cd /Users/$LOGGED_IN_USER && '$APPDIR/Fig.app/Contents/MacOS/fig-darwin-universal' '_' 'brew-uninstall')\nsudo rm -rf \"$APPDIR/Fig.app\"\nsudo rm -rf 'fig'\ntrash $LOGGED_IN_USER '~/.fig'\ntrash $LOGGED_IN_USER '~/.fig.dotfiles.bak'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.mschrage.fig'\ntrash $LOGGED_IN_USER '~/Library/Application Support/fig'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.mschrage.fig'\ntrash $LOGGED_IN_USER '~/Library/Caches/fig'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.mschrage.fig'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.mschrage.fig.*'\ntrash $LOGGED_IN_USER '~/Library/WebKit/com.mschrage.fig'\n"
(cd "/Users/$LOGGED_IN_USER" && "${APPDIR}Fig.app/Contents/MacOS/fig-darwin-universal" '_' 'brew-uninstall')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/maintained-apps/outputs/fig/darwin.json` at line 20, The uninstall hook
invocation is using a single-quoted executable path, so the APPDIR variable is
not expanded and the brew-uninstall command never runs. Update the Fig uninstall
call near the existing fig-darwin-universal execution so the path is evaluated
with the APPDIR value before invoking the hook, and keep the surrounding
LOGGED_IN_USER handling unchanged.

],
"refs": {
"9cf9c318": "#!/bin/bash\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nexpand_pkgid_and_map() {\n local PKGID=\"$1\"\n local FUNC=\"$2\"\n if [[ \"$PKGID\" == *\"*\" ]]; then\n local prefix=\"${PKGID%\\*}\"\n echo \"Expanding wildcard for PKGID: $PKGID\"\n for receipt in $(pkgutil --pkgs | grep \"^${prefix}\"); do\n echo \"Processing $receipt\"\n \"$FUNC\" \"$receipt\"\n done\n else\n \"$FUNC\" \"$PKGID\"\n fi\n}\n\nforget_pkg() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" forget_receipt\n}\n\nforget_receipt() {\n local PKGID=\"$1\"\n sudo pkgutil --forget \"$PKGID\"\n}\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service}\"\n else\n launchctl remove \"${service}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service}.plist\"\n \"/Library/LaunchDaemons/${service}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n}\n\nremove_pkg_files() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" remove_receipt_files\n}\n\nremove_receipt_files() {\n local PKGID=\"$1\"\n local PKGINFO VOLUME INSTALL_LOCATION FULL_INSTALL_LOCATION\n\n echo \"pkgutil --pkg-info-plist \\\"$PKGID\\\"\"\n PKGINFO=$(pkgutil --pkg-info-plist \"$PKGID\")\n VOLUME=$(echo \"$PKGINFO\" | awk '/<key>volume<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n INSTALL_LOCATION=$(echo \"$PKGINFO\" | awk '/<key>install-location<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n\n if [ -z \"$INSTALL_LOCATION\" ] || [ \"$INSTALL_LOCATION\" = \"/\" ]; then\n FULL_INSTALL_LOCATION=\"$VOLUME\"\n else\n FULL_INSTALL_LOCATION=\"$VOLUME/$INSTALL_LOCATION\"\n FULL_INSTALL_LOCATION=$(echo \"$FULL_INSTALL_LOCATION\" | sed 's|//|/|g')\n fi\n\n echo \"sudo pkgutil --only-files --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-files --files \"$PKGID\" | sed \"s|^|/${INSTALL_LOCATION}/|\" | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n echo \"sudo pkgutil --only-dirs --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | grep '\\\\.app$' | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-dirs --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | grep '\\.app$' | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n root_app_dir=$(\n sudo pkgutil --only-dirs --files \"$PKGID\" \\\n | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" \\\n | grep 'Applications' \\\n | awk '{ print length, $0 }' \\\n | sort -n \\\n | head -n1 \\\n | cut -d' ' -f2-\n )\n if [ -n \"$root_app_dir\" ]; then\n echo \"sudo rmdir -p \\\"$root_app_dir\\\" 2>/dev/null || :\"\n sudo rmdir -p \"$root_app_dir\" 2>/dev/null || :\n fi\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.malwarebytes.mbam.frontend.agent'\nremove_launchctl_service 'com.malwarebytes.mbam.rtprotection.daemon'\nremove_launchctl_service 'com.malwarebytes.mbam.settings.daemon'\nquit_application 'com.malwarebytes.mbam.frontend.agent'\nremove_pkg_files 'com.malwarebytes.mbam.*'\nforget_pkg 'com.malwarebytes.mbam.*'\nsudo rm -rf '/Library/Application Support/Malwarebytes/MBAM'\nsudo rmdir '/Library/Application Support/Malwarebytes'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.malwarebytes.mbam.frontend.*'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.crashlytics.data/com.malwarebytes.mbam.frontend.*'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.malwarebytes.mbam.frontend.*'\ntrash $LOGGED_IN_USER '~/Library/Caches/io.fabric.sdk.mac.data/com.malwarebytes.mbam.frontend.*'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.malwarebytes.mbam.frontend.*'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/com.malwarebytes.mbam.frontend.application.savedState'\n",
"ba6e6e70": "#!/bin/bash\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nexpand_pkgid_and_map() {\n local PKGID=\"$1\"\n local FUNC=\"$2\"\n if [[ \"$PKGID\" == *\"*\" ]]; then\n local prefix=\"${PKGID%\\*}\"\n echo \"Expanding wildcard for PKGID: $PKGID\"\n for receipt in $(pkgutil --pkgs | grep \"^${prefix}\"); do\n echo \"Processing $receipt\"\n \"$FUNC\" \"$receipt\"\n done\n else\n \"$FUNC\" \"$PKGID\"\n fi\n}\n\nforget_pkg() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" forget_receipt\n}\n\nforget_receipt() {\n local PKGID=\"$1\"\n sudo pkgutil --forget \"$PKGID\"\n}\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n # A wildcard label can't be used with launchctl or as a plist name, so expand\n # it to the labels of currently loaded services that match the pattern.\n local services=(\"$service\")\n if [[ \"$service\" == *\"*\"* ]]; then\n local regex\n # Escape regex metacharacters, turn '*' into '.*', and anchor the pattern so\n # it matches a full label rather than a substring.\n regex=$(printf '%s' \"$service\" | sed -e 's/[][(){}.^$+?|\\\\]/\\\\&/g' -e 's/\\*/.*/g')\n regex=\"^${regex}$\"\n services=()\n local id\n # Match every loaded job by label regardless of PID; launchctl list reports\n # loaded-but-not-running jobs with a \"-\" in the PID column.\n while read -r _ _ id; do\n [[ \"$id\" =~ $regex ]] && services+=(\"$id\")\n done < <(launchctl list 2>/dev/null | tail -n +2)\n if [[ ${#services[@]} -eq 0 ]]; then\n echo \"No loaded launchctl service matches ${service}\"\n return\n fi\n fi\n\n local service_label\n for service_label in \"${services[@]}\"; do\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service_label}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service_label}\"\n else\n launchctl remove \"${service_label}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service_label}.plist\"\n \"/Library/LaunchDaemons/${service_label}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n done\n}\n\nremove_pkg_files() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" remove_receipt_files\n}\n\nremove_receipt_files() {\n local PKGID=\"$1\"\n local PKGINFO VOLUME INSTALL_LOCATION FULL_INSTALL_LOCATION\n\n echo \"pkgutil --pkg-info-plist \\\"$PKGID\\\"\"\n PKGINFO=$(pkgutil --pkg-info-plist \"$PKGID\")\n VOLUME=$(echo \"$PKGINFO\" | awk '/<key>volume<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n INSTALL_LOCATION=$(echo \"$PKGINFO\" | awk '/<key>install-location<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n\n if [ -z \"$INSTALL_LOCATION\" ] || [ \"$INSTALL_LOCATION\" = \"/\" ]; then\n FULL_INSTALL_LOCATION=\"$VOLUME\"\n else\n FULL_INSTALL_LOCATION=\"$VOLUME/$INSTALL_LOCATION\"\n FULL_INSTALL_LOCATION=$(echo \"$FULL_INSTALL_LOCATION\" | sed 's|//|/|g')\n fi\n\n echo \"sudo pkgutil --only-files --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-files --files \"$PKGID\" | sed \"s|^|/${INSTALL_LOCATION}/|\" | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n echo \"sudo pkgutil --only-dirs --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | grep '\\\\.app$' | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-dirs --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | grep '\\.app$' | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n root_app_dir=$(\n sudo pkgutil --only-dirs --files \"$PKGID\" \\\n | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" \\\n | grep 'Applications' \\\n | awk '{ print length, $0 }' \\\n | sort -n \\\n | head -n1 \\\n | cut -d' ' -f2-\n )\n if [ -n \"$root_app_dir\" ]; then\n echo \"sudo rmdir -p \\\"$root_app_dir\\\" 2>/dev/null || :\"\n sudo rmdir -p \"$root_app_dir\" 2>/dev/null || :\n fi\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.malwarebytes.mbam.frontend.agent'\nremove_launchctl_service 'com.malwarebytes.mbam.rtprotection.daemon'\nremove_launchctl_service 'com.malwarebytes.mbam.settings.daemon'\nquit_application 'com.malwarebytes.mbam.frontend.agent'\nremove_pkg_files 'com.malwarebytes.mbam.*'\nforget_pkg 'com.malwarebytes.mbam.*'\nsudo rm -rf '/Library/Application Support/Malwarebytes/MBAM'\nsudo rmdir '/Library/Application Support/Malwarebytes'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.malwarebytes.mbam.frontend.*'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.crashlytics.data/com.malwarebytes.mbam.frontend.*'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.malwarebytes.mbam.frontend.*'\ntrash $LOGGED_IN_USER '~/Library/Caches/io.fabric.sdk.mac.data/com.malwarebytes.mbam.frontend.*'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.malwarebytes.mbam.frontend.*'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/com.malwarebytes.mbam.frontend.application.savedState'\n",

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Quit the Malwarebytes app bundle before removing its files.

The maintained-app query identifies com.malwarebytes.mbam.frontend.application, but the uninstall script only quits com.malwarebytes.mbam.frontend.agent. Add the application bundle ID so the UI process is stopped before package and support-file cleanup.

Proposed fix
 remove_launchctl_service 'com.malwarebytes.mbam.settings.daemon'
+quit_application 'com.malwarebytes.mbam.frontend.application'
 quit_application 'com.malwarebytes.mbam.frontend.agent'
 remove_pkg_files 'com.malwarebytes.mbam.*'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"ba6e6e70": "#!/bin/bash\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nexpand_pkgid_and_map() {\n local PKGID=\"$1\"\n local FUNC=\"$2\"\n if [[ \"$PKGID\" == *\"*\" ]]; then\n local prefix=\"${PKGID%\\*}\"\n echo \"Expanding wildcard for PKGID: $PKGID\"\n for receipt in $(pkgutil --pkgs | grep \"^${prefix}\"); do\n echo \"Processing $receipt\"\n \"$FUNC\" \"$receipt\"\n done\n else\n \"$FUNC\" \"$PKGID\"\n fi\n}\n\nforget_pkg() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" forget_receipt\n}\n\nforget_receipt() {\n local PKGID=\"$1\"\n sudo pkgutil --forget \"$PKGID\"\n}\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n # A wildcard label can't be used with launchctl or as a plist name, so expand\n # it to the labels of currently loaded services that match the pattern.\n local services=(\"$service\")\n if [[ \"$service\" == *\"*\"* ]]; then\n local regex\n # Escape regex metacharacters, turn '*' into '.*', and anchor the pattern so\n # it matches a full label rather than a substring.\n regex=$(printf '%s' \"$service\" | sed -e 's/[][(){}.^$+?|\\\\]/\\\\&/g' -e 's/\\*/.*/g')\n regex=\"^${regex}$\"\n services=()\n local id\n # Match every loaded job by label regardless of PID; launchctl list reports\n # loaded-but-not-running jobs with a \"-\" in the PID column.\n while read -r _ _ id; do\n [[ \"$id\" =~ $regex ]] && services+=(\"$id\")\n done < <(launchctl list 2>/dev/null | tail -n +2)\n if [[ ${#services[@]} -eq 0 ]]; then\n echo \"No loaded launchctl service matches ${service}\"\n return\n fi\n fi\n\n local service_label\n for service_label in \"${services[@]}\"; do\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service_label}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service_label}\"\n else\n launchctl remove \"${service_label}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service_label}.plist\"\n \"/Library/LaunchDaemons/${service_label}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n done\n}\n\nremove_pkg_files() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" remove_receipt_files\n}\n\nremove_receipt_files() {\n local PKGID=\"$1\"\n local PKGINFO VOLUME INSTALL_LOCATION FULL_INSTALL_LOCATION\n\n echo \"pkgutil --pkg-info-plist \\\"$PKGID\\\"\"\n PKGINFO=$(pkgutil --pkg-info-plist \"$PKGID\")\n VOLUME=$(echo \"$PKGINFO\" | awk '/<key>volume<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n INSTALL_LOCATION=$(echo \"$PKGINFO\" | awk '/<key>install-location<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n\n if [ -z \"$INSTALL_LOCATION\" ] || [ \"$INSTALL_LOCATION\" = \"/\" ]; then\n FULL_INSTALL_LOCATION=\"$VOLUME\"\n else\n FULL_INSTALL_LOCATION=\"$VOLUME/$INSTALL_LOCATION\"\n FULL_INSTALL_LOCATION=$(echo \"$FULL_INSTALL_LOCATION\" | sed 's|//|/|g')\n fi\n\n echo \"sudo pkgutil --only-files --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-files --files \"$PKGID\" | sed \"s|^|/${INSTALL_LOCATION}/|\" | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n echo \"sudo pkgutil --only-dirs --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | grep '\\\\.app$' | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-dirs --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | grep '\\.app$' | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n root_app_dir=$(\n sudo pkgutil --only-dirs --files \"$PKGID\" \\\n | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" \\\n | grep 'Applications' \\\n | awk '{ print length, $0 }' \\\n | sort -n \\\n | head -n1 \\\n | cut -d' ' -f2-\n )\n if [ -n \"$root_app_dir\" ]; then\n echo \"sudo rmdir -p \\\"$root_app_dir\\\" 2>/dev/null || :\"\n sudo rmdir -p \"$root_app_dir\" 2>/dev/null || :\n fi\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.malwarebytes.mbam.frontend.agent'\nremove_launchctl_service 'com.malwarebytes.mbam.rtprotection.daemon'\nremove_launchctl_service 'com.malwarebytes.mbam.settings.daemon'\nquit_application 'com.malwarebytes.mbam.frontend.agent'\nremove_pkg_files 'com.malwarebytes.mbam.*'\nforget_pkg 'com.malwarebytes.mbam.*'\nsudo rm -rf '/Library/Application Support/Malwarebytes/MBAM'\nsudo rmdir '/Library/Application Support/Malwarebytes'\ntrash $LOGGED_IN_USER '~/Library/Application Support/com.malwarebytes.mbam.frontend.*'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.crashlytics.data/com.malwarebytes.mbam.frontend.*'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.malwarebytes.mbam.frontend.*'\ntrash $LOGGED_IN_USER '~/Library/Caches/io.fabric.sdk.mac.data/com.malwarebytes.mbam.frontend.*'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.malwarebytes.mbam.frontend.*'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/com.malwarebytes.mbam.frontend.application.savedState'\n",
remove_launchctl_service 'com.malwarebytes.mbam.settings.daemon'
quit_application 'com.malwarebytes.mbam.frontend.application'
quit_application 'com.malwarebytes.mbam.frontend.agent'
remove_pkg_files 'com.malwarebytes.mbam.*'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/maintained-apps/outputs/malwarebytes/darwin.json` at line 19, The
uninstall script only quits the agent and leaves the main Malwarebytes UI bundle
running, so stop the application bundle too before file cleanup. Update the
uninstall flow near quit_application/remove_launchctl_service to also call
quit_application for com.malwarebytes.mbam.frontend.application, using the
existing quit_application helper so the app bundle is terminated before
remove_pkg_files, forget_pkg, and trash run.

],
"refs": {
"24478eff": "#!/bin/bash\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nexpand_pkgid_and_map() {\n local PKGID=\"$1\"\n local FUNC=\"$2\"\n if [[ \"$PKGID\" == *\"*\" ]]; then\n local prefix=\"${PKGID%\\*}\"\n echo \"Expanding wildcard for PKGID: $PKGID\"\n for receipt in $(pkgutil --pkgs | grep \"^${prefix}\"); do\n echo \"Processing $receipt\"\n \"$FUNC\" \"$receipt\"\n done\n else\n \"$FUNC\" \"$PKGID\"\n fi\n}\n\nforget_pkg() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" forget_receipt\n}\n\nforget_receipt() {\n local PKGID=\"$1\"\n sudo pkgutil --forget \"$PKGID\"\n}\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service}\"\n else\n launchctl remove \"${service}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service}.plist\"\n \"/Library/LaunchDaemons/${service}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n}\n\nremove_pkg_files() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" remove_receipt_files\n}\n\nremove_receipt_files() {\n local PKGID=\"$1\"\n local PKGINFO VOLUME INSTALL_LOCATION FULL_INSTALL_LOCATION\n\n echo \"pkgutil --pkg-info-plist \\\"$PKGID\\\"\"\n PKGINFO=$(pkgutil --pkg-info-plist \"$PKGID\")\n VOLUME=$(echo \"$PKGINFO\" | awk '/<key>volume<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n INSTALL_LOCATION=$(echo \"$PKGINFO\" | awk '/<key>install-location<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n\n if [ -z \"$INSTALL_LOCATION\" ] || [ \"$INSTALL_LOCATION\" = \"/\" ]; then\n FULL_INSTALL_LOCATION=\"$VOLUME\"\n else\n FULL_INSTALL_LOCATION=\"$VOLUME/$INSTALL_LOCATION\"\n FULL_INSTALL_LOCATION=$(echo \"$FULL_INSTALL_LOCATION\" | sed 's|//|/|g')\n fi\n\n echo \"sudo pkgutil --only-files --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-files --files \"$PKGID\" | sed \"s|^|/${INSTALL_LOCATION}/|\" | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n echo \"sudo pkgutil --only-dirs --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | grep '\\\\.app$' | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-dirs --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | grep '\\.app$' | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n root_app_dir=$(\n sudo pkgutil --only-dirs --files \"$PKGID\" \\\n | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" \\\n | grep 'Applications' \\\n | awk '{ print length, $0 }' \\\n | sort -n \\\n | head -n1 \\\n | cut -d' ' -f2-\n )\n if [ -n \"$root_app_dir\" ]; then\n echo \"sudo rmdir -p \\\"$root_app_dir\\\" 2>/dev/null || :\"\n sudo rmdir -p \"$root_app_dir\" 2>/dev/null || :\n fi\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.microsoft.autoupdate.helper'\nremove_launchctl_service 'com.microsoft.autoupdate.helpertool'\nremove_launchctl_service 'com.microsoft.update.agent'\nquit_application 'com.microsoft.autoupdate.fba'\nquit_application 'com.microsoft.autoupdate2'\nquit_application 'com.microsoft.errorreporting'\nremove_pkg_files 'com.microsoft.package.Microsoft_AU_Bootstrapper.app'\nforget_pkg 'com.microsoft.package.Microsoft_AU_Bootstrapper.app'\nremove_pkg_files 'com.microsoft.package.Microsoft_AutoUpdate.app'\nforget_pkg 'com.microsoft.package.Microsoft_AutoUpdate.app'\nsudo rm -rf '/Library/Caches/com.microsoft.autoupdate.fba'\nsudo rm -rf '/Library/Caches/com.microsoft.autoupdate.helper'\nsudo rm -rf '/Library/LaunchDaemons/com.microsoft.autoupdate.helper.plist'\nsudo rm -rf '/Library/Preferences/com.microsoft.autoupdate2.plist'\nsudo rm -rf '/Library/PrivilegedHelperTools/com.microsoft.autoupdate.helper'\nsudo rmdir '~/Library/Caches/Microsoft'\nsudo rmdir '~/Library/Caches/Microsoft/uls'\ntrash $LOGGED_IN_USER '~/Library/Application Scripts/UBF8T346G9.com.microsoft.oneauth'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Microsoft AutoUpdate'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.microsoft.autoupdate.fba'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.microsoft.autoupdate2'\ntrash $LOGGED_IN_USER '~/Library/Caches/Microsoft/uls/com.microsoft.autoupdate.fba'\ntrash $LOGGED_IN_USER '~/Library/Caches/Microsoft/uls/com.microsoft.autoupdate2'\ntrash $LOGGED_IN_USER '~/Library/Cookies/com.microsoft.autoupdate.fba.binarycookies'\ntrash $LOGGED_IN_USER '~/Library/Cookies/com.microsoft.autoupdate2.binarycookies'\ntrash $LOGGED_IN_USER '~/Library/Group Containers/UBF8T346G9.com.microsoft.oneauth'\ntrash $LOGGED_IN_USER '~/Library/Group Containers/UBF8T346G9.ms'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.microsoft.autoupdate.fba'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.microsoft.autoupdate2'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.microsoft.autoupdate.fba.plist'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.microsoft.autoupdate2.plist'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.microsoft.shared.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/com.microsoft.autoupdate2.savedState'\n",
"31e9e039": "#!/bin/bash\n\n# variables\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nexpand_pkgid_and_map() {\n local PKGID=\"$1\"\n local FUNC=\"$2\"\n if [[ \"$PKGID\" == *\"*\" ]]; then\n local prefix=\"${PKGID%\\*}\"\n echo \"Expanding wildcard for PKGID: $PKGID\"\n for receipt in $(pkgutil --pkgs | grep \"^${prefix}\"); do\n echo \"Processing $receipt\"\n \"$FUNC\" \"$receipt\"\n done\n else\n \"$FUNC\" \"$PKGID\"\n fi\n}\n\nforget_pkg() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" forget_receipt\n}\n\nforget_receipt() {\n local PKGID=\"$1\"\n sudo pkgutil --forget \"$PKGID\"\n}\n\nquit_application() {\n local bundle_id=\"$1\"\n local timeout_duration=10\n\n # check if the application is running\n local app_running\n app_running=$(osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null)\n if [[ \"$app_running\" != \"true\" ]]; then\n return\n fi\n\n local console_user\n console_user=$(stat -f \"%Su\" /dev/console)\n if [[ -z \"$console_user\" || \"$console_user\" == \"root\" || \"$console_user\" == \"loginwindow\" ]]; then\n echo \"Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'.\"\n return\n fi\n\n echo \"Quitting application '$bundle_id'...\"\n\n # try to quit the application within the timeout period\n local quit_success=false\n SECONDS=0\n while (( SECONDS < timeout_duration )); do\n if osascript -e \"tell application id \\\"$bundle_id\\\" to quit\" >/dev/null 2>&1; then\n if ! pgrep -f \"$bundle_id\" >/dev/null 2>&1; then\n echo \"Application '$bundle_id' quit successfully.\"\n quit_success=true\n break\n fi\n fi\n sleep 1\n done\n\n if [[ \"$quit_success\" = false ]]; then\n echo \"Application '$bundle_id' did not quit.\"\n fi\n}\n\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n # A wildcard label can't be used with launchctl or as a plist name, so expand\n # it to the labels of currently loaded services that match the pattern.\n local services=(\"$service\")\n if [[ \"$service\" == *\"*\"* ]]; then\n local regex\n # Escape regex metacharacters, turn '*' into '.*', and anchor the pattern so\n # it matches a full label rather than a substring.\n regex=$(printf '%s' \"$service\" | sed -e 's/[][(){}.^$+?|\\\\]/\\\\&/g' -e 's/\\*/.*/g')\n regex=\"^${regex}$\"\n services=()\n local id\n # Match every loaded job by label regardless of PID; launchctl list reports\n # loaded-but-not-running jobs with a \"-\" in the PID column.\n while read -r _ _ id; do\n [[ \"$id\" =~ $regex ]] && services+=(\"$id\")\n done < <(launchctl list 2>/dev/null | tail -n +2)\n if [[ ${#services[@]} -eq 0 ]]; then\n echo \"No loaded launchctl service matches ${service}\"\n return\n fi\n fi\n\n local service_label\n for service_label in \"${services[@]}\"; do\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service_label}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service_label}\"\n else\n launchctl remove \"${service_label}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service_label}.plist\"\n \"/Library/LaunchDaemons/${service_label}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n done\n}\n\nremove_pkg_files() {\n local PKGID=\"$1\"\n expand_pkgid_and_map \"$PKGID\" remove_receipt_files\n}\n\nremove_receipt_files() {\n local PKGID=\"$1\"\n local PKGINFO VOLUME INSTALL_LOCATION FULL_INSTALL_LOCATION\n\n echo \"pkgutil --pkg-info-plist \\\"$PKGID\\\"\"\n PKGINFO=$(pkgutil --pkg-info-plist \"$PKGID\")\n VOLUME=$(echo \"$PKGINFO\" | awk '/<key>volume<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n INSTALL_LOCATION=$(echo \"$PKGINFO\" | awk '/<key>install-location<\\/key>/ {getline; gsub(/.*<string>|<\\/string>.*/, \"\"); print}')\n\n if [ -z \"$INSTALL_LOCATION\" ] || [ \"$INSTALL_LOCATION\" = \"/\" ]; then\n FULL_INSTALL_LOCATION=\"$VOLUME\"\n else\n FULL_INSTALL_LOCATION=\"$VOLUME/$INSTALL_LOCATION\"\n FULL_INSTALL_LOCATION=$(echo \"$FULL_INSTALL_LOCATION\" | sed 's|//|/|g')\n fi\n\n echo \"sudo pkgutil --only-files --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-files --files \"$PKGID\" | sed \"s|^|/${INSTALL_LOCATION}/|\" | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n echo \"sudo pkgutil --only-dirs --files \\\"$PKGID\\\" | sed \\\"s|^|${FULL_INSTALL_LOCATION}/|\\\" | grep '\\\\.app$' | tr '\\\\\\\\n' '\\\\\\\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\"\n sudo pkgutil --only-dirs --files \"$PKGID\" | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" | grep '\\.app$' | tr '\\n' '\\0' | /usr/bin/sudo -u root -E -- /usr/bin/xargs -0 -- /bin/rm -rf\n\n root_app_dir=$(\n sudo pkgutil --only-dirs --files \"$PKGID\" \\\n | sed \"s|^|${FULL_INSTALL_LOCATION}/|\" \\\n | grep 'Applications' \\\n | awk '{ print length, $0 }' \\\n | sort -n \\\n | head -n1 \\\n | cut -d' ' -f2-\n )\n if [ -n \"$root_app_dir\" ]; then\n echo \"sudo rmdir -p \\\"$root_app_dir\\\" 2>/dev/null || :\"\n sudo rmdir -p \"$root_app_dir\" 2>/dev/null || :\n fi\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.microsoft.autoupdate.helper'\nremove_launchctl_service 'com.microsoft.autoupdate.helpertool'\nremove_launchctl_service 'com.microsoft.update.agent'\nquit_application 'com.microsoft.autoupdate.fba'\nquit_application 'com.microsoft.autoupdate2'\nquit_application 'com.microsoft.errorreporting'\nremove_pkg_files 'com.microsoft.package.Microsoft_AU_Bootstrapper.app'\nforget_pkg 'com.microsoft.package.Microsoft_AU_Bootstrapper.app'\nremove_pkg_files 'com.microsoft.package.Microsoft_AutoUpdate.app'\nforget_pkg 'com.microsoft.package.Microsoft_AutoUpdate.app'\nsudo rm -rf '/Library/Caches/com.microsoft.autoupdate.fba'\nsudo rm -rf '/Library/Caches/com.microsoft.autoupdate.helper'\nsudo rm -rf '/Library/LaunchDaemons/com.microsoft.autoupdate.helper.plist'\nsudo rm -rf '/Library/Preferences/com.microsoft.autoupdate2.plist'\nsudo rm -rf '/Library/PrivilegedHelperTools/com.microsoft.autoupdate.helper'\nsudo rmdir '~/Library/Caches/Microsoft'\nsudo rmdir '~/Library/Caches/Microsoft/uls'\ntrash $LOGGED_IN_USER '~/Library/Application Scripts/UBF8T346G9.com.microsoft.oneauth'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Microsoft AutoUpdate'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.microsoft.autoupdate.fba'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.microsoft.autoupdate2'\ntrash $LOGGED_IN_USER '~/Library/Caches/Microsoft/uls/com.microsoft.autoupdate.fba'\ntrash $LOGGED_IN_USER '~/Library/Caches/Microsoft/uls/com.microsoft.autoupdate2'\ntrash $LOGGED_IN_USER '~/Library/Cookies/com.microsoft.autoupdate.fba.binarycookies'\ntrash $LOGGED_IN_USER '~/Library/Cookies/com.microsoft.autoupdate2.binarycookies'\ntrash $LOGGED_IN_USER '~/Library/Group Containers/UBF8T346G9.com.microsoft.oneauth'\ntrash $LOGGED_IN_USER '~/Library/Group Containers/UBF8T346G9.ms'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.microsoft.autoupdate.fba'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.microsoft.autoupdate2'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.microsoft.autoupdate.fba.plist'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.microsoft.autoupdate2.plist'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.microsoft.shared.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/com.microsoft.autoupdate2.savedState'\n",

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not delete shared Microsoft group containers from the AutoUpdate uninstaller.

UBF8T346G9.com.microsoft.oneauth and UBF8T346G9.ms are shared Microsoft app containers; removing them during an AutoUpdate uninstall can disrupt other installed Microsoft apps. Scope cleanup to AutoUpdate-specific paths or guard this behind “no other Microsoft apps installed.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/maintained-apps/outputs/microsoft-auto-update/darwin.json` at line 19, The
AutoUpdate uninstaller is deleting shared Microsoft group containers that other
Microsoft apps rely on. Update the cleanup logic in the script around trash()
calls so `UBF8T346G9.com.microsoft.oneauth` and `UBF8T346G9.ms` are not removed
by default; restrict cleanup to AutoUpdate-specific paths or add a check before
those removals to only proceed when no other Microsoft apps are installed.

],
"refs": {
"3b06f51c": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service}\"\n else\n launchctl remove \"${service}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service}.plist\"\n \"/Library/LaunchDaemons/${service}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.microsoft.EdgeUpdater.wake'\nsudo rm -rf \"$APPDIR/Microsoft Edge.app\"\nsudo rmdir '~/Library/Application Support/Microsoft'\nsudo rmdir '~/Library/Microsoft'\ntrash $LOGGED_IN_USER '~/Library/Application Scripts/com.microsoft.edgemac.wdgExtension'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Microsoft Edge'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Microsoft/EdgeUpdater'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.microsoft.edgemac'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.microsoft.EdgeUpdater'\ntrash $LOGGED_IN_USER '~/Library/Caches/Microsoft Edge'\ntrash $LOGGED_IN_USER '~/Library/Containers/com.microsoft.edgemac.wdgExtension'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.microsoft.edgemac'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.microsoft.EdgeUpdater'\ntrash $LOGGED_IN_USER '~/Library/LaunchAgents/com.microsoft.EdgeUpdater.*.plist'\ntrash $LOGGED_IN_USER '~/Library/Microsoft/MicrosoftSoftwareUpdate/Actives/com.microsoft.edgemac'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.microsoft.edgemac.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/com.microsoft.edgemac.savedState'\ntrash $LOGGED_IN_USER '~/Library/WebKit/com.microsoft.edgemac'\n",
"afe8f338": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n # A wildcard label can't be used with launchctl or as a plist name, so expand\n # it to the labels of currently loaded services that match the pattern.\n local services=(\"$service\")\n if [[ \"$service\" == *\"*\"* ]]; then\n local regex\n # Escape regex metacharacters, turn '*' into '.*', and anchor the pattern so\n # it matches a full label rather than a substring.\n regex=$(printf '%s' \"$service\" | sed -e 's/[][(){}.^$+?|\\\\]/\\\\&/g' -e 's/\\*/.*/g')\n regex=\"^${regex}$\"\n services=()\n local id\n # Match every loaded job by label regardless of PID; launchctl list reports\n # loaded-but-not-running jobs with a \"-\" in the PID column.\n while read -r _ _ id; do\n [[ \"$id\" =~ $regex ]] && services+=(\"$id\")\n done < <(launchctl list 2>/dev/null | tail -n +2)\n if [[ ${#services[@]} -eq 0 ]]; then\n echo \"No loaded launchctl service matches ${service}\"\n return\n fi\n fi\n\n local service_label\n for service_label in \"${services[@]}\"; do\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service_label}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service_label}\"\n else\n launchctl remove \"${service_label}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service_label}.plist\"\n \"/Library/LaunchDaemons/${service_label}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.microsoft.EdgeUpdater.wake'\nsudo rm -rf \"$APPDIR/Microsoft Edge.app\"\nsudo rmdir '~/Library/Application Support/Microsoft'\nsudo rmdir '~/Library/Microsoft'\ntrash $LOGGED_IN_USER '~/Library/Application Scripts/com.microsoft.edgemac.wdgExtension'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Microsoft Edge'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Microsoft/EdgeUpdater'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.microsoft.edgemac'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.microsoft.EdgeUpdater'\ntrash $LOGGED_IN_USER '~/Library/Caches/Microsoft Edge'\ntrash $LOGGED_IN_USER '~/Library/Containers/com.microsoft.edgemac.wdgExtension'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.microsoft.edgemac'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.microsoft.EdgeUpdater'\ntrash $LOGGED_IN_USER '~/Library/LaunchAgents/com.microsoft.EdgeUpdater.*.plist'\ntrash $LOGGED_IN_USER '~/Library/Microsoft/MicrosoftSoftwareUpdate/Actives/com.microsoft.edgemac'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.microsoft.edgemac.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/com.microsoft.edgemac.savedState'\ntrash $LOGGED_IN_USER '~/Library/WebKit/com.microsoft.edgemac'\n",

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the wildcard helper for EdgeUpdater labels.

The script now removes only com.microsoft.EdgeUpdater.wake, but EdgeUpdater jobs use suffixed labels such as com.microsoft.EdgeUpdater.wake.system and update labels. Use the new wildcard path so those jobs are actually removed.

Proposed fix
-remove_launchctl_service 'com.microsoft.EdgeUpdater.wake'
+remove_launchctl_service 'com.microsoft.EdgeUpdater.*'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"afe8f338": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\n\nremove_launchctl_service() {\n local service=\"$1\"\n local booleans=(\"true\" \"false\")\n local plist_status\n local paths\n local should_sudo\n\n echo \"Removing launchctl service ${service}\"\n\n # A wildcard label can't be used with launchctl or as a plist name, so expand\n # it to the labels of currently loaded services that match the pattern.\n local services=(\"$service\")\n if [[ \"$service\" == *\"*\"* ]]; then\n local regex\n # Escape regex metacharacters, turn '*' into '.*', and anchor the pattern so\n # it matches a full label rather than a substring.\n regex=$(printf '%s' \"$service\" | sed -e 's/[][(){}.^$+?|\\\\]/\\\\&/g' -e 's/\\*/.*/g')\n regex=\"^${regex}$\"\n services=()\n local id\n # Match every loaded job by label regardless of PID; launchctl list reports\n # loaded-but-not-running jobs with a \"-\" in the PID column.\n while read -r _ _ id; do\n [[ \"$id\" =~ $regex ]] && services+=(\"$id\")\n done < <(launchctl list 2>/dev/null | tail -n +2)\n if [[ ${#services[@]} -eq 0 ]]; then\n echo \"No loaded launchctl service matches ${service}\"\n return\n fi\n fi\n\n local service_label\n for service_label in \"${services[@]}\"; do\n for should_sudo in \"${booleans[@]}\"; do\n plist_status=$(launchctl list \"${service_label}\" 2>/dev/null)\n\n if [[ $plist_status == \\{* ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo launchctl remove \"${service_label}\"\n else\n launchctl remove \"${service_label}\"\n fi\n sleep 1\n fi\n\n paths=(\n \"/Library/LaunchAgents/${service_label}.plist\"\n \"/Library/LaunchDaemons/${service_label}.plist\"\n )\n\n # if not using sudo, prepend the home directory to the paths\n if [[ $should_sudo == \"false\" ]]; then\n for i in \"${!paths[@]}\"; do\n paths[i]=\"${HOME}${paths[i]}\"\n done\n fi\n\n for path in \"${paths[@]}\"; do\n if [[ -e \"$path\" ]]; then\n if [[ $should_sudo == \"true\" ]]; then\n sudo rm -f -- \"$path\"\n else\n rm -f -- \"$path\"\n fi\n fi\n done\n done\n done\n}\n\ntrash() {\n local logged_in_user=\"$1\"\n local target_file=\"$2\"\n local timestamp=\"$(date +%Y-%m-%d-%s)\"\n local rand=\"$(jot -r 1 0 99999)\"\n\n # replace ~ with /Users/$logged_in_user\n if [[ \"$target_file\" == ~* ]]; then\n target_file=\"/Users/$logged_in_user${target_file:1}\"\n fi\n\n local trash=\"/Users/$logged_in_user/.Trash\"\n\n # If the target contains glob characters, expand it and move each match.\n if [[ \"$target_file\" == *[*?[]* ]]; then\n local file file_name\n local matched=false\n local i=0\n # compgen -G expands the (quoted) pattern itself, so paths containing\n # spaces glob correctly; reading line by line keeps each match intact.\n while IFS= read -r file; do\n [[ -n \"$file\" ]] || continue\n [[ -e \"$file\" || -L \"$file\" ]] || continue\n matched=true\n i=$((i + 1))\n file_name=\"$(basename \"$file\")\"\n echo \"removing $file.\"\n # The per-match counter keeps matches that share a basename from\n # overwriting each other in the trash.\n mv -f \"$file\" \"$trash/${file_name}_${timestamp}_${rand}_${i}\"\n done < <(compgen -G \"$target_file\" 2>/dev/null)\n if [[ \"$matched\" == false ]]; then\n echo \"$target_file doesn't exist.\"\n fi\n return\n fi\n\n local file_name=\"$(basename \"${target_file}\")\"\n\n if [[ -e \"$target_file\" ]]; then\n echo \"removing $target_file.\"\n mv -f \"$target_file\" \"$trash/${file_name}_${timestamp}_${rand}\"\n else\n echo \"$target_file doesn't exist.\"\n fi\n}\n\nremove_launchctl_service 'com.microsoft.EdgeUpdater.wake'\nsudo rm -rf \"$APPDIR/Microsoft Edge.app\"\nsudo rmdir '~/Library/Application Support/Microsoft'\nsudo rmdir '~/Library/Microsoft'\ntrash $LOGGED_IN_USER '~/Library/Application Scripts/com.microsoft.edgemac.wdgExtension'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Microsoft Edge'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Microsoft/EdgeUpdater'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.microsoft.edgemac'\ntrash $LOGGED_IN_USER '~/Library/Caches/com.microsoft.EdgeUpdater'\ntrash $LOGGED_IN_USER '~/Library/Caches/Microsoft Edge'\ntrash $LOGGED_IN_USER '~/Library/Containers/com.microsoft.edgemac.wdgExtension'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.microsoft.edgemac'\ntrash $LOGGED_IN_USER '~/Library/HTTPStorages/com.microsoft.EdgeUpdater'\ntrash $LOGGED_IN_USER '~/Library/LaunchAgents/com.microsoft.EdgeUpdater.*.plist'\ntrash $LOGGED_IN_USER '~/Library/Microsoft/MicrosoftSoftwareUpdate/Actives/com.microsoft.edgemac'\ntrash $LOGGED_IN_USER '~/Library/Preferences/com.microsoft.edgemac.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/com.microsoft.edgemac.savedState'\ntrash $LOGGED_IN_USER '~/Library/WebKit/com.microsoft.edgemac'\n",
remove_launchctl_service 'com.microsoft.EdgeUpdater.*'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ee/maintained-apps/outputs/microsoft-edge/darwin.json` at line 19, The
EdgeUpdater cleanup still targets only a single fixed label, so suffixed jobs
like the wake.system and other update variants are left behind. Update the Edge
removal flow in remove_launchctl_service to use the wildcard-capable path
already implemented there, and change the EdgeUpdater invocation to pass the
broader label pattern so it expands to all matching loaded services before
removal.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Closing in favor of #48597.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: test-fma-pr-only

Failed stage: Filter apps.json and verify changed apps [❌]

Failed test name: ""

Failure summary:

The action failed during the app validation step because it could not download the maintained
installer for AdGuard (adguard/darwin).
- The download of
https://static.adguard.com/mac/release/AdGuard-2.18.0.2089.dmg timed out with context deadline
exceeded (see log line 5177).
- The validator reported Apps with errors: [AdGuard] and exited with
exit status 1 (lines 7021-7023), causing the workflow to fail even though 116/117 apps validated
successfully.

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

168:  * [new branch]            11938-loadtesting-branch    -> origin/11938-loadtesting-branch
169:  * [new branch]            12007-ui-idp-metadata       -> origin/12007-ui-idp-metadata
170:  * [new branch]            12351-try-fix-for-fleet-desktop -> origin/12351-try-fix-for-fleet-desktop
171:  * [new branch]            12356-puppet-callback       -> origin/12356-puppet-callback
172:  * [new branch]            12375-query-bulk-action     -> origin/12375-query-bulk-action
173:  * [new branch]            12474-documentation         -> origin/12474-documentation
174:  * [new branch]            12614-federated-auth-support -> origin/12614-federated-auth-support
175:  * [new branch]            12687-host-issues-query     -> origin/12687-host-issues-query
176:  * [new branch]            12696-loadtest-cis-changes  -> origin/12696-loadtest-cis-changes
177:  * [new branch]            12842-fleetd-bitlocker-management -> origin/12842-fleetd-bitlocker-management
178:  * [new branch]            12842-fleetd-bitlocker-table -> origin/12842-fleetd-bitlocker-table
179:  * [new branch]            12889-faster-software       -> origin/12889-faster-software
180:  * [new branch]            13287-loadtest-changes      -> origin/13287-loadtest-changes
181:  * [new branch]            13288-poc-fleet-fips        -> origin/13288-poc-fleet-fips
182:  * [new branch]            13485-query-reports-migration -> origin/13485-query-reports-migration
183:  * [new branch]            13574-cache-failed-policy-counts -> origin/13574-cache-failed-policy-counts
184:  * [new branch]            13657-docs                  -> origin/13657-docs
...

196:  * [new branch]            146orch                     -> origin/146orch
197:  * [new branch]            14753-windows-ps1-api       -> origin/14753-windows-ps1-api
198:  * [new branch]            14957-test-dep-screen       -> origin/14957-test-dep-screen
199:  * [new branch]            14957-test-dep-screen-patch -> origin/14957-test-dep-screen-patch
200:  * [new branch]            15068-disk-encryption-ii    -> origin/15068-disk-encryption-ii
201:  * [new branch]            15082-download-api-docs     -> origin/15082-download-api-docs
202:  * [new branch]            15082-make-endpoints-consistent -> origin/15082-make-endpoints-consistent
203:  * [new branch]            15082-update-download-endpoints -> origin/15082-update-download-endpoints
204:  * [new branch]            15380-hosts-api-doc         -> origin/15380-hosts-api-doc
205:  * [new branch]            15461-ui-dep-assign-profile-responses -> origin/15461-ui-dep-assign-profile-responses
206:  * [new branch]            15463-msi-fix               -> origin/15463-msi-fix
207:  * [new branch]            15559-move-scep             -> origin/15559-move-scep
208:  * [new branch]            15633                       -> origin/15633
209:  * [new branch]            15770-mac-os-vuln           -> origin/15770-mac-os-vuln
210:  * [new branch]            15801-migration             -> origin/15801-migration
211:  * [new branch]            15912-password-reset-error-code -> origin/15912-password-reset-error-code
212:  * [new branch]            16182-log-root              -> origin/16182-log-root
...

618:  * [new branch]            4701-tickets                -> origin/4701-tickets
619:  * [new branch]            4701-with-osqpfix           -> origin/4701-with-osqpfix
620:  * [new branch]            471-0716                    -> origin/471-0716
621:  * [new branch]            471-cloudfront              -> origin/471-cloudfront
622:  * [new branch]            471-tickets                 -> origin/471-tickets
623:  * [new branch]            4710mdm                     -> origin/4710mdm
624:  * [new branch]            4710mdmloadtest             -> origin/4710mdmloadtest
625:  * [new branch]            4711-bsln                   -> origin/4711-bsln
626:  * [new branch]            471RC-07-09                 -> origin/471RC-07-09
627:  * [new branch]            4720mdm                     -> origin/4720mdm
628:  * [new branch]            4720orch                    -> origin/4720orch
629:  * [new branch]            4720orch1                   -> origin/4720orch1
630:  * [new branch]            4720orch2                   -> origin/4720orch2
631:  * [new branch]            47289-new-fma-masv          -> origin/47289-new-fma-masv
632:  * [new branch]            472orchm                    -> origin/472orchm
633:  * [new branch]            47303-redis-moved-errors-from-getqueryresultscounts-incrqueryresultscounts-in-cluster-mode -> origin/47303-redis-moved-errors-from-getqueryresultscounts-incrqueryresultscounts-in-cluster-mode
634:  * [new branch]            4735orch                    -> origin/4735orch
...

663:  * [new branch]            7765-frontend               -> origin/7765-frontend
664:  * [new branch]            7766-backend-final-main-merge -> origin/7766-backend-final-main-merge
665:  * [new branch]            7766-backend-test           -> origin/7766-backend-test
666:  * [new branch]            7766-frontend               -> origin/7766-frontend
667:  * [new branch]            7993-figure-out-the-limits-of-our-current-search-approach -> origin/7993-figure-out-the-limits-of-our-current-search-approach
668:  * [new branch]            8021-orbit-use-specified-identifier -> origin/8021-orbit-use-specified-identifier
669:  * [new branch]            8186-vulnerable-software-false-positive-docker -> origin/8186-vulnerable-software-false-positive-docker
670:  * [new branch]            8593-gitops                 -> origin/8593-gitops
671:  * [new branch]            8708-docs                   -> origin/8708-docs
672:  * [new branch]            8708-encryption-key-api     -> origin/8708-encryption-key-api
673:  * [new branch]            8708-ingest-file-vault      -> origin/8708-ingest-file-vault
674:  * [new branch]            8708-ingest-macos-encription-key -> origin/8708-ingest-macos-encription-key
675:  * [new branch]            8708-ingest-macos-encription-key-ii -> origin/8708-ingest-macos-encription-key-ii
676:  * [new branch]            8974-native-smbios-uuid-support -> origin/8974-native-smbios-uuid-support
677:  * [new branch]            9260-cis-checks-5.10.x      -> origin/9260-cis-checks-5.10.x
678:  * [new branch]            9394-failed-to-create-device-auth-token-due-to-duplicate-key-errors -> origin/9394-failed-to-create-device-auth-token-due-to-duplicate-key-errors
679:  * [new branch]            9590-schema                 -> origin/9590-schema
...

869:  * [new branch]            allenhouchins-add-hardware-procurement-info -> origin/allenhouchins-add-hardware-procurement-info
870:  * [new branch]            allenhouchins-article-formatting-update -> origin/allenhouchins-article-formatting-update
871:  * [new branch]            allenhouchins-contour-normalize -> origin/allenhouchins-contour-normalize
872:  * [new branch]            allenhouchins-fma-installer-arch -> origin/allenhouchins-fma-installer-arch
873:  * [new branch]            allenhouchins-net-arm64     -> origin/allenhouchins-net-arm64
874:  * [new branch]            allenhouchins-new-blog-post -> origin/allenhouchins-new-blog-post
875:  * [new branch]            allenhouchins-patch-1       -> origin/allenhouchins-patch-1
876:  * [new branch]            allenhouchins-santa-table-updates -> origin/allenhouchins-santa-table-updates
877:  * [new branch]            allenhouchins-update-custom-tap -> origin/allenhouchins-update-custom-tap
878:  * [new branch]            allenhouchins-update-new-fma-skill -> origin/allenhouchins-update-new-fma-skill
879:  * [new branch]            always-create-event-next-thu -> origin/always-create-event-next-thu
880:  * [new branch]            and_osq_proj                -> origin/and_osq_proj
881:  * [new branch]            andrey/loadtest-oval-vulns  -> origin/andrey/loadtest-oval-vulns
882:  * [new branch]            android-cofniguration-profiles-research -> origin/android-cofniguration-profiles-research
883:  * [new branch]            android-enroll-note         -> origin/android-enroll-note
884:  * [new branch]            android-enrollment-error    -> origin/android-enrollment-error
885:  * [new branch]            android-fleet-vars          -> origin/android-fleet-vars
...

939:  * [new branch]            await-configuration-docs    -> origin/await-configuration-docs
940:  * [new branch]            aws-licensing-integration-proposal -> origin/aws-licensing-integration-proposal
941:  * [new branch]            backport-a41fb63            -> origin/backport-a41fb63
942:  * [new branch]            backport-c02af              -> origin/backport-c02af
943:  * [new branch]            backport-e65d6cf            -> origin/backport-e65d6cf
944:  * [new branch]            backport-ef07a406cc8dbccb4914bbd27631df74e5b13b49 -> origin/backport-ef07a406cc8dbccb4914bbd27631df74e5b13b49
945:  * [new branch]            backport-vpp-fix            -> origin/backport-vpp-fix
946:  * [new branch]            base-software-titles-loadtest -> origin/base-software-titles-loadtest
947:  * [new branch]            base-software-titles-loadtest-2 -> origin/base-software-titles-loadtest-2
948:  * [new branch]            beekeeper-fma-initial       -> origin/beekeeper-fma-initial
949:  * [new branch]            bettapizza                  -> origin/bettapizza
950:  * [new branch]            bettapizza--ghost-testimonial-update -> origin/bettapizza--ghost-testimonial-update
951:  * [new branch]            bettapizza-patch-1          -> origin/bettapizza-patch-1
952:  * [new branch]            bettapizza-patch-2          -> origin/bettapizza-patch-2
953:  * [new branch]            bettapizza-patch-3          -> origin/bettapizza-patch-3
954:  * [new branch]            better-byod-error           -> origin/better-byod-error
955:  * [new branch]            bounded-context-proposals   -> origin/bounded-context-proposals
...

990:  * [new branch]            cherry-pick-48012-to-docs-v4.89.0 -> origin/cherry-pick-48012-to-docs-v4.89.0
991:  * [new branch]            cherry-pick-48561-into-4.89 -> origin/cherry-pick-48561-into-4.89
992:  * [new branch]            cherry-pick-8750-into-rc-minor-fleet-v4.60.0 -> origin/cherry-pick-8750-into-rc-minor-fleet-v4.60.0
993:  * [new branch]            cherry-pick-dogfood-change  -> origin/cherry-pick-dogfood-change
994:  * [new branch]            cherry-pick-dogfood-env-vars -> origin/cherry-pick-dogfood-env-vars
995:  * [new branch]            cherry-pick-fleetctl-changes-to-allow-testing -> origin/cherry-pick-fleetctl-changes-to-allow-testing
996:  * [new branch]            cherry-pick-mac-address-vital -> origin/cherry-pick-mac-address-vital
997:  * [new branch]            cherry-pick-patches         -> origin/cherry-pick-patches
998:  * [new branch]            cherry-pick-remove-overrides-in-dev-mode -> origin/cherry-pick-remove-overrides-in-dev-mode
999:  * [new branch]            cherry-pick-util            -> origin/cherry-pick-util
1000:  * [new branch]            cherry-pick-webhooks        -> origin/cherry-pick-webhooks
1001:  * [new branch]            cherrypick-v4.85.0-version-bump-into-main -> origin/cherrypick-v4.85.0-version-bump-into-main
1002:  * [new branch]            chore-UI-update-for-CSP     -> origin/chore-UI-update-for-CSP
1003:  * [new branch]            chore-add-specific-yarn-version -> origin/chore-add-specific-yarn-version
1004:  * [new branch]            chore-backend-api-patterns  -> origin/chore-backend-api-patterns
1005:  * [new branch]            chore-change-error-message-custom-profile -> origin/chore-change-error-message-custom-profile
1006:  * [new branch]            chore-cp-chage-status-code-turn-off-mdm -> origin/chore-cp-chage-status-code-turn-off-mdm
...

1194:  * [new branch]            edwardsb-vuln-processing-fix-sandcastle -> origin/edwardsb-vuln-processing-fix-sandcastle
1195:  * [new branch]            edwardsb-vuln-processing-volume-fix -> origin/edwardsb-vuln-processing-volume-fix
1196:  * [new branch]            edwardsb/deployment-guide-updates -> origin/edwardsb/deployment-guide-updates
1197:  * [new branch]            edwardsb/restrict_console_access -> origin/edwardsb/restrict_console_access
1198:  * [new branch]            elastic_rum                 -> origin/elastic_rum
1199:  * [new branch]            enable-jit-by-default       -> origin/enable-jit-by-default
1200:  * [new branch]            enable-test-go-automation-macos -> origin/enable-test-go-automation-macos
1201:  * [new branch]            enforce-firefox-doh         -> origin/enforce-firefox-doh
1202:  * [new branch]            enforce-firefox-windows     -> origin/enforce-firefox-windows
1203:  * [new branch]            enforce-ios-26.5-update-deadline-july-2026 -> origin/enforce-ios-26.5-update-deadline-july-2026
1204:  * [new branch]            enforce-iso27001-workstations -> origin/enforce-iso27001-workstations
1205:  * [new branch]            enforce-macos-15.5-update-deadline -> origin/enforce-macos-15.5-update-deadline
1206:  * [new branch]            enforce-macos-version-workstations -> origin/enforce-macos-version-workstations
1207:  * [new branch]            enroll-android-feature-guide -> origin/enroll-android-feature-guide
1208:  * [new branch]            ericswenson0-patch-1        -> origin/ericswenson0-patch-1
1209:  * [new branch]            error-counts                -> origin/error-counts
1210:  * [new branch]            errors-perf-testing         -> origin/errors-perf-testing
1211:  * [new branch]            es-reports                  -> origin/es-reports
...

1227:  * [new branch]            eugkuo-patch-6-slugs-FMA    -> origin/eugkuo-patch-6-slugs-FMA
1228:  * [new branch]            example-fleetctl-profiles   -> origin/example-fleetctl-profiles
1229:  * [new branch]            exp-mdbook-docs             -> origin/exp-mdbook-docs
1230:  * [new branch]            experiment-disable-some-deletes -> origin/experiment-disable-some-deletes
1231:  * [new branch]            experiment-hardcode-counters-to-zero -> origin/experiment-hardcode-counters-to-zero
1232:  * [new branch]            experiment-no-txs           -> origin/experiment-no-txs
1233:  * [new branch]            experiment-status-tooltip   -> origin/experiment-status-tooltip
1234:  * [new branch]            experiment-with-two-branches -> origin/experiment-with-two-branches
1235:  * [new branch]            experimental_moveitem       -> origin/experimental_moveitem
1236:  * [new branch]            experimient-no-counts       -> origin/experimient-no-counts
1237:  * [new branch]            expiredcertstool            -> origin/expiredcertstool
1238:  * [new branch]            express-vpn-win             -> origin/express-vpn-win
1239:  * [new branch]            extra-dep-logs              -> origin/extra-dep-logs
1240:  * [new branch]            f84ee4c5f4                  -> origin/f84ee4c5f4
1241:  * [new branch]            faf-doc-clarification       -> origin/faf-doc-clarification
1242:  * [new branch]            failing-test                -> origin/failing-test
1243:  * [new branch]            faq-updates-2026-06         -> origin/faq-updates-2026-06
...

1283:  * [new branch]            feature-hydrant-ca          -> origin/feature-hydrant-ca
1284:  * [new branch]            feature-idp-byod            -> origin/feature-idp-byod
1285:  * [new branch]            feature-orbit-find          -> origin/feature-orbit-find
1286:  * [new branch]            feature-prioritization      -> origin/feature-prioritization
1287:  * [new branch]            feature-v4.44.1-live-queries -> origin/feature-v4.44.1-live-queries
1288:  * [new branch]            feature-windows-mdm-policy-extraction -> origin/feature-windows-mdm-policy-extraction
1289:  * [new branch]            feature/add-blank-to-osquer-link -> origin/feature/add-blank-to-osquer-link
1290:  * [new branch]            feature/fleet-macos-password-sync -> origin/feature/fleet-macos-password-sync
1291:  * [new branch]            feature_14722-activity-feed-webhooks -> origin/feature_14722-activity-feed-webhooks
1292:  * [new branch]            feature_18115-crit-vuln-issues -> origin/feature_18115-crit-vuln-issues
1293:  * [new branch]            feature_19010-ipad-ios-lock-wipe-backend-changes -> origin/feature_19010-ipad-ios-lock-wipe-backend-changes
1294:  * [new branch]            feature_22078               -> origin/feature_22078
1295:  * [new branch]            fix                         -> origin/fix
1296:  * [new branch]            fix-35118-cis-win10-v4      -> origin/fix-35118-cis-win10-v4
1297:  * [new branch]            fix-4.84.4-k8s              -> origin/fix-4.84.4-k8s
1298:  * [new branch]            fix-40083-failed-install-retries -> origin/fix-40083-failed-install-retries
1299:  * [new branch]            fix-41290-vpp-added-status  -> origin/fix-41290-vpp-added-status
1300:  * [new branch]            fix-41337-save-env-secrets-dry-run -> origin/fix-41337-save-env-secrets-dry-run
1301:  * [new branch]            fix-42901-ticker-leak       -> origin/fix-42901-ticker-leak
1302:  * [new branch]            fix-43623-fma-patch-policy-gitops -> origin/fix-43623-fma-patch-policy-gitops
1303:  * [new branch]            fix-44199-embedded-app-bundles -> origin/fix-44199-embedded-app-bundles
1304:  * [new branch]            fix-44325-gitops-mode-tooltips -> origin/fix-44325-gitops-mode-tooltips
1305:  * [new branch]            fix-47388-getclientconfig-nil-map -> origin/fix-47388-getclientconfig-nil-map
1306:  * [new branch]            fix-android-unenroll-activities -> origin/fix-android-unenroll-activities
1307:  * [new branch]            fix-blockquotes-google-groups -> origin/fix-blockquotes-google-groups
1308:  * [new branch]            fix-ca-verdict-spoof-16386  -> origin/fix-ca-verdict-spoof-16386
1309:  * [new branch]            fix-case-in-deploy-config-docs -> origin/fix-case-in-deploy-config-docs
1310:  * [new branch]            fix-cert-details-modal      -> origin/fix-cert-details-modal
1311:  * [new branch]            fix-checkerboard-scroll-to-right -> origin/fix-checkerboard-scroll-to-right
1312:  * [new branch]            fix-checkerboard-tooltip-nowrap -> origin/fix-checkerboard-tooltip-nowrap
1313:  * [new branch]            fix-cloudflare-warp-fma-output-json -> origin/fix-cloudflare-warp-fma-output-json
1314:  * [new branch]            fix-codeql-ci-error         -> origin/fix-codeql-ci-error
1315:  * [new branch]            fix-colima                  -> origin/fix-colima
1316:  * [new branch]            fix-color-for-mobile-sticky-nav -> origin/fix-color-for-mobile-sticky-nav
1317:  * [new branch]            fix-conditional-access-observer-authz -> origin/fix-conditional-access-observer-authz
1318:  * [new branch]            fix-conflict-in-feature_19010-ipad-ios-lock-wipe -> origin/fix-conflict-in-feature_19010-ipad-ios-lock-wipe
1319:  * [new branch]            fix-contrib-docs            -> origin/fix-contrib-docs
1320:  * [new branch]            fix-cve-2026-2792-firefox-esr -> origin/fix-cve-2026-2792-firefox-esr
1321:  * [new branch]            fix-cve-validate-enrichment-canary -> origin/fix-cve-validate-enrichment-canary
1322:  * [new branch]            fix-docker-cleanup-quay-nonfatal -> origin/fix-docker-cleanup-quay-nonfatal
1323:  * [new branch]            fix-docker-publish          -> origin/fix-docker-publish
1324:  * [new branch]            fix-dogfood-workstation     -> origin/fix-dogfood-workstation
1325:  * [new branch]            fix-embedded-bundle-titles-44199 -> origin/fix-embedded-bundle-titles-44199
1326:  * [new branch]            fix-firefox-cve-2026-2792   -> origin/fix-firefox-cve-2026-2792
1327:  * [new branch]            fix-firefox-fma-conflict-error-msg -> origin/fix-firefox-fma-conflict-error-msg
1328:  * [new branch]            fix-flakey-orbit-test       -> origin/fix-flakey-orbit-test
...

1334:  * [new branch]            fix-host-counts-vpp-software -> origin/fix-host-counts-vpp-software
1335:  * [new branch]            fix-instability-ubuntu-latest -> origin/fix-instability-ubuntu-latest
1336:  * [new branch]            fix-ios-ipados-enrollment-label-42721 -> origin/fix-ios-ipados-enrollment-label-42721
1337:  * [new branch]            fix-list-sw-permissions     -> origin/fix-list-sw-permissions
1338:  * [new branch]            fix-mdm-enrolled-webhook    -> origin/fix-mdm-enrolled-webhook
1339:  * [new branch]            fix-mdm-redirect            -> origin/fix-mdm-redirect
1340:  * [new branch]            fix-mdm-url-truncation      -> origin/fix-mdm-url-truncation
1341:  * [new branch]            fix-missing-test-schema-update -> origin/fix-missing-test-schema-update
1342:  * [new branch]            fix-modernize               -> origin/fix-modernize
1343:  * [new branch]            fix-no-space-left-on-device-failure -> origin/fix-no-space-left-on-device-failure
1344:  * [new branch]            fix-notarization            -> origin/fix-notarization
1345:  * [new branch]            fix-orbit-notarization      -> origin/fix-orbit-notarization
1346:  * [new branch]            fix-os-settings-name-col-truncation -> origin/fix-os-settings-name-col-truncation
1347:  * [new branch]            fix-os-settings-underline-descender-clip -> origin/fix-os-settings-underline-descender-clip
1348:  * [new branch]            fix-pricing-more-info-icon-documentationurl -> origin/fix-pricing-more-info-icon-documentationurl
1349:  * [new branch]            fix-receive-from-github-error -> origin/fix-receive-from-github-error
1350:  * [new branch]            fix-sandbox-local           -> origin/fix-sandbox-local
...

1364:  * [new branch]            fix-ui-typo-fleet-maintained-empty -> origin/fix-ui-typo-fleet-maintained-empty
1365:  * [new branch]            fix-vpp-edit-fleet-desktop-message -> origin/fix-vpp-edit-fleet-desktop-message
1366:  * [new branch]            fix-whatsapp-version-check  -> origin/fix-whatsapp-version-check
1367:  * [new branch]            fix-win-os-version          -> origin/fix-win-os-version
1368:  * [new branch]            fix_14825                   -> origin/fix_14825
1369:  * [new branch]            fixes-on-minor-fleet-v4.54.0 -> origin/fixes-on-minor-fleet-v4.54.0
1370:  * [new branch]            fixes-plus-4.35.1           -> origin/fixes-plus-4.35.1
1371:  * [new branch]            flaky-tests-nov-6           -> origin/flaky-tests-nov-6
1372:  * [new branch]            fleet-desktop-linux-browser -> origin/fleet-desktop-linux-browser
1373:  * [new branch]            fleet-desktop-version       -> origin/fleet-desktop-version
1374:  * [new branch]            fleet-mdm-server-url        -> origin/fleet-mdm-server-url
1375:  * [new branch]            fleet-multiple-abm-vpp-proposal -> origin/fleet-multiple-abm-vpp-proposal
1376:  * [new branch]            fleet-premium-users-usage-statistics -> origin/fleet-premium-users-usage-statistics
1377:  * [new branch]            fleet-release-infra         -> origin/fleet-release-infra
1378:  * [new branch]            fleet-server-config-1817    -> origin/fleet-server-config-1817
1379:  * [new branch]            fleet-server-log-request-errors -> origin/fleet-server-log-request-errors
1380:  * [new branch]            fleet-ship-rebuild          -> origin/fleet-ship-rebuild
...

2389:  * [new branch]            hotfix-failing-tests        -> origin/hotfix-failing-tests
2390:  * [new branch]            hotfix-query-update         -> origin/hotfix-query-update
2391:  * [new branch]            hotfix-revert-sofa-tables   -> origin/hotfix-revert-sofa-tables
2392:  * [new branch]            hotfix-v4.67.2-migration    -> origin/hotfix-v4.67.2-migration
2393:  * [new branch]            hotfix-v4.76.1              -> origin/hotfix-v4.76.1
2394:  * [new branch]            hover-gravatar              -> origin/hover-gravatar
2395:  * [new branch]            hughestaylor-patch-1        -> origin/hughestaylor-patch-1
2396:  * [new branch]            hughestaylor-patch-1-1      -> origin/hughestaylor-patch-1-1
2397:  * [new branch]            ignore-fleetctl-vulnerabilities -> origin/ignore-fleetctl-vulnerabilities
2398:  * [new branch]            improve-flakey-test         -> origin/improve-flakey-test
2399:  * [new branch]            improve-prometheus-metrics  -> origin/improve-prometheus-metrics
2400:  * [new branch]            improve-settings-subnav     -> origin/improve-settings-subnav
2401:  * [new branch]            include-mnt-in-gads-metric  -> origin/include-mnt-in-gads-metric
2402:  * [new branch]            increase-goreleaser-timeout-to-60m -> origin/increase-goreleaser-timeout-to-60m
2403:  * [new branch]            ingress-gateway-support     -> origin/ingress-gateway-support
2404:  * [new branch]            insert-software-installers-retry-error -> origin/insert-software-installers-retry-error
2405:  * [new branch]            install-all-recently-installed -> origin/install-all-recently-installed
...

2410:  * [new branch]            ireedy-patch-2              -> origin/ireedy-patch-2
2411:  * [new branch]            ireedy-patch-3              -> origin/ireedy-patch-3
2412:  * [new branch]            irena-deebradelo            -> origin/irena-deebradelo
2413:  * [new branch]            irena-handbook-edit         -> origin/irena-handbook-edit
2414:  * [new branch]            irena-test-with-john        -> origin/irena-test-with-john
2415:  * [new branch]            irenareedy-event-process    -> origin/irenareedy-event-process
2416:  * [new branch]            irenareedy-patch-1          -> origin/irenareedy-patch-1
2417:  * [new branch]            irenareedy-patch-2          -> origin/irenareedy-patch-2
2418:  * [new branch]            irenareedy-patch-4          -> origin/irenareedy-patch-4
2419:  * [new branch]            irenareedy-thumbtack-case-study -> origin/irenareedy-thumbtack-case-study
2420:  * [new branch]            iso-27001-compliance        -> origin/iso-27001-compliance
2421:  * [new branch]            iso42001-ai-discovery-inventory -> origin/iso42001-ai-discovery-inventory
2422:  * [new branch]            issue-11545-my-device-dep-modal -> origin/issue-11545-my-device-dep-modal
2423:  * [new branch]            issue-2716-specify-query-platforms -> origin/issue-2716-specify-query-platforms
2424:  * [new branch]            issue-3271-rate-limit-distributed-writes -> origin/issue-3271-rate-limit-distributed-writes
2425:  * [new branch]            issue-4361-mail-change-should-error -> origin/issue-4361-mail-change-should-error
2426:  * [new branch]            issue-conf-1968-delete-uninstalled-software-from-main -> origin/issue-conf-1968-delete-uninstalled-software-from-main
...

2635:  * [new branch]            melpike-api-apple-declarations-38986 -> origin/melpike-api-apple-declarations-38986
2636:  * [new branch]            melpike-api-default-fleet-windows-41787 -> origin/melpike-api-default-fleet-windows-41787
2637:  * [new branch]            melpike-api-device-url-43895 -> origin/melpike-api-device-url-43895
2638:  * [new branch]            melpike-api-minimum-version-39085 -> origin/melpike-api-minimum-version-39085
2639:  * [new branch]            melpike-api-py-script-41470 -> origin/melpike-api-py-script-41470
2640:  * [new branch]            melpike-audit-log-account-provisioning-45524 -> origin/melpike-audit-log-account-provisioning-45524
2641:  * [new branch]            melpike-audit-log-mdm-cmd-45021 -> origin/melpike-audit-log-mdm-cmd-45021
2642:  * [new branch]            melpike-audit-recovery-lock-password-rotate -> origin/melpike-audit-recovery-lock-password-rotate
2643:  * [new branch]            melpike-byod-wipe-lock-23242 -> origin/melpike-byod-wipe-lock-23242
2644:  * [new branch]            melpike-byod-wipe-lock-23242-1 -> origin/melpike-byod-wipe-lock-23242-1
2645:  * [new branch]            melpike-byod-wipe-lock-api-23242 -> origin/melpike-byod-wipe-lock-api-23242
2646:  * [new branch]            melpike-byod-wipe-lock-yaml-23242 -> origin/melpike-byod-wipe-lock-yaml-23242
2647:  * [new branch]            melpike-combine-guides-30674 -> origin/melpike-combine-guides-30674
2648:  * [new branch]            melpike-create-managed-local-account-guide-37141 -> origin/melpike-create-managed-local-account-guide-37141
2649:  * [new branch]            melpike-default-fleet-windows-41787 -> origin/melpike-default-fleet-windows-41787
2650:  * [new branch]            melpike-dep-error-messaging-43916 -> origin/melpike-dep-error-messaging-43916
2651:  * [new branch]            melpike-doc-deploy-sw-41470 -> origin/melpike-doc-deploy-sw-41470
...

2799:  * [new branch]            mna-doc-change-api-structure -> origin/mna-doc-change-api-structure
2800:  * [new branch]            mna-experiment-macos-profiles-uuid -> origin/mna-experiment-macos-profiles-uuid
2801:  * [new branch]            mna-fix-22558-windows-installer-stuck-pending -> origin/mna-fix-22558-windows-installer-stuck-pending
2802:  * [new branch]            mna-fix-duplicate-lock-unlock-activity -> origin/mna-fix-duplicate-lock-unlock-activity
2803:  * [new branch]            mna-fix-failing-tests       -> origin/mna-fix-failing-tests
2804:  * [new branch]            mna-fix-flaky-integration-test -> origin/mna-fix-flaky-integration-test
2805:  * [new branch]            mna-fix-oom-fleetctl-test   -> origin/mna-fix-oom-fleetctl-test
2806:  * [new branch]            mna-loadtest-enroll-host-limit -> origin/mna-loadtest-enroll-host-limit
2807:  * [new branch]            mna-orbit-node-key          -> origin/mna-orbit-node-key
2808:  * [new branch]            mna-revert-change-to-autogenerated-activities-doc -> origin/mna-revert-change-to-autogenerated-activities-doc
2809:  * [new branch]            mna-temp-osquery-perf-loadtest -> origin/mna-temp-osquery-perf-loadtest
2810:  * [new branch]            mna11997                    -> origin/mna11997
2811:  * [new branch]            mock-associate-assets-endpoint -> origin/mock-associate-assets-endpoint
2812:  * [new branch]            monitoring-test             -> origin/monitoring-test
2813:  * [new branch]            more-conditional-access-api-updates -> origin/more-conditional-access-api-updates
2814:  * [new branch]            more-fd-errors              -> origin/more-fd-errors
2815:  * [new branch]            move-conditional-access-script-to-testing-qa -> origin/move-conditional-access-script-to-testing-qa
...

3379:  * [new branch]            sgress454/34259-end-user-auth-setup-backend -> origin/sgress454/34259-end-user-auth-setup-backend
3380:  * [new branch]            sgress454/34528-end-user-auth-setup-agent -> origin/sgress454/34528-end-user-auth-setup-agent
3381:  * [new branch]            sgress454/35376             -> origin/sgress454/35376
3382:  * [new branch]            sgress454/35452             -> origin/sgress454/35452
3383:  * [new branch]            sgress454/37127-open-macos-end-user-auth-window -> origin/sgress454/37127-open-macos-end-user-auth-window
3384:  * [new branch]            sgress454/38063-skeleton-server -> origin/sgress454/38063-skeleton-server
3385:  * [new branch]            sgress454/39344-rename-teams-to-fleet-all-changes -> origin/sgress454/39344-rename-teams-to-fleet-all-changes
3386:  * [new branch]            sgress454/39344-rename-teams-to-fleet-api-changes -> origin/sgress454/39344-rename-teams-to-fleet-api-changes
3387:  * [new branch]            sgress454/40642-update-jit-saml-prefix -> origin/sgress454/40642-update-jit-saml-prefix
3388:  * [new branch]            sgress454/43482-re-pin      -> origin/sgress454/43482-re-pin
3389:  * [new branch]            sgress454/44077-disable-datasets-backend -> origin/sgress454/44077-disable-datasets-backend
3390:  * [new branch]            sgress454/44077-disable-datasets-docs -> origin/sgress454/44077-disable-datasets-docs
3391:  * [new branch]            sgress454/44077-disable-datasets-frontend -> origin/sgress454/44077-disable-datasets-frontend
3392:  * [new branch]            sgress454/45290-drop-mobile-from-charting -> origin/sgress454/45290-drop-mobile-from-charting
3393:  * [new branch]            sgress454/add-vex-for-cve-2026-23517 -> origin/sgress454/add-vex-for-cve-2026-23517
3394:  * [new branch]            sgress454/adr-for-error-codes -> origin/sgress454/adr-for-error-codes
3395:  * [new branch]            sgress454/adr-refactor-list-hosts -> origin/sgress454/adr-refactor-list-hosts
...

3447:  * [new branch]            software-titles-loadtest-2  -> origin/software-titles-loadtest-2
3448:  * [new branch]            solarized-theme             -> origin/solarized-theme
3449:  * [new branch]            solutions-folder            -> origin/solutions-folder
3450:  * [new branch]            sort-seen-hosts-before-inserting -> origin/sort-seen-hosts-before-inserting
3451:  * [new branch]            spalmesano0-patch-2         -> origin/spalmesano0-patch-2
3452:  * [new branch]            spike-fleet-lint-plugin     -> origin/spike-fleet-lint-plugin
3453:  * [new branch]            spike-ui-permissions-pattern -> origin/spike-ui-permissions-pattern
3454:  * [new branch]            spokanemac-add-passcode-ddm-profile -> origin/spokanemac-add-passcode-ddm-profile
3455:  * [new branch]            spokanemac-dogfood-additional-apps -> origin/spokanemac-dogfood-additional-apps
3456:  * [new branch]            spokanemac-handbook-release-article -> origin/spokanemac-handbook-release-article
3457:  * [new branch]            spokanemac-macdevops-conference-post -> origin/spokanemac-macdevops-conference-post
3458:  * [new branch]            spokanemac-meetup-template  -> origin/spokanemac-meetup-template
3459:  * [new branch]            spokanemac-migrating-fleetdm-from-dogfood-to-terraform-on-aws -> origin/spokanemac-migrating-fleetdm-from-dogfood-to-terraform-on-aws
3460:  * [new branch]            spokanemac-release-article-issue-template -> origin/spokanemac-release-article-issue-template
3461:  * [new branch]            step4-improvements          -> origin/step4-improvements
3462:  * [new branch]            store-query-execution-and-ingestion-errors -> origin/store-query-execution-and-ingestion-errors
3463:  * [new branch]            support-breaking-osquery-downgrades -> origin/support-breaking-osquery-downgrades
...

3530:  * [new branch]            ui-self-service-sg-5-21     -> origin/ui-self-service-sg-5-21
3531:  * [new branch]            unauth-installers           -> origin/unauth-installers
3532:  * [new branch]            uniform-brex-spending-limit -> origin/uniform-brex-spending-limit
3533:  * [new branch]            untx-software               -> origin/untx-software
3534:  * [new branch]            upcoming-activities-mdm-commands -> origin/upcoming-activities-mdm-commands
3535:  * [new branch]            upcoming-commands           -> origin/upcoming-commands
3536:  * [new branch]            update-1password-and-safari-policy-versions-2601200025 -> origin/update-1password-and-safari-policy-versions-2601200025
3537:  * [new branch]            update-1password-and-safari-policy-versions-2601200610 -> origin/update-1password-and-safari-policy-versions-2601200610
3538:  * [new branch]            update-1password-and-safari-policy-versions-2602261818 -> origin/update-1password-and-safari-policy-versions-2602261818
3539:  * [new branch]            update-1password-and-safari-policy-versions-2603101819 -> origin/update-1password-and-safari-policy-versions-2603101819
3540:  * [new branch]            update-1password-and-safari-policy-versions-2604080037 -> origin/update-1password-and-safari-policy-versions-2604080037
3541:  * [new branch]            update-1password-and-safari-policy-versions-2604080637 -> origin/update-1password-and-safari-policy-versions-2604080637
3542:  * [new branch]            update-Cloudflare-WARP-FMA-with-UpgradeCode -> origin/update-Cloudflare-WARP-FMA-with-UpgradeCode
3543:  * [new branch]            update-article-format       -> origin/update-article-format
3544:  * [new branch]            update-bug-report-template  -> origin/update-bug-report-template
3545:  * [new branch]            update-build-script-error-message -> origin/update-build-script-error-message
3546:  * [new branch]            update-cancel-and-done-links -> origin/update-cancel-and-done-links
...

3626:  * [new branch]            update-solutions-folder     -> origin/update-solutions-folder
3627:  * [new branch]            update-testing-qa-apps-2511191525 -> origin/update-testing-qa-apps-2511191525
3628:  * [new branch]            update-tuf-keys             -> origin/update-tuf-keys
3629:  * [new branch]            update-tuf-md               -> origin/update-tuf-md
3630:  * [new branch]            update-usage-statistics-mobile-devices -> origin/update-usage-statistics-mobile-devices
3631:  * [new branch]            update-user-password-length-validation-test -> origin/update-user-password-length-validation-test
3632:  * [new branch]            update-vex-statements       -> origin/update-vex-statements
3633:  * [new branch]            update-why-this-way         -> origin/update-why-this-way
3634:  * [new branch]            upgrade-eslint              -> origin/upgrade-eslint
3635:  * [new branch]            upgrade-go                  -> origin/upgrade-go
3636:  * [new branch]            upgrade-typescript          -> origin/upgrade-typescript
3637:  * [new branch]            upload-msiexec-install-log  -> origin/upload-msiexec-install-log
3638:  * [new branch]            usage-stats-mobile-devices-count -> origin/usage-stats-mobile-devices-count
3639:  * [new branch]            use-dockerhub-creds         -> origin/use-dockerhub-creds
3640:  * [new branch]            use-software-name-helper    -> origin/use-software-name-helper
3641:  * [new branch]            user-channel-error          -> origin/user-channel-error
3642:  * [new branch]            users-table-experiment      -> origin/users-table-experiment
...

3794:  * [new branch]            wesite-fix-sales-rituals    -> origin/wesite-fix-sales-rituals
3795:  * [new branch]            willmayhone88-patch-1       -> origin/willmayhone88-patch-1
3796:  * [new branch]            win-fma-deepl               -> origin/win-fma-deepl
3797:  * [new branch]            win-fma-evernote            -> origin/win-fma-evernote
3798:  * [new branch]            win-fma-goland              -> origin/win-fma-goland
3799:  * [new branch]            win-fma-testing             -> origin/win-fma-testing
3800:  * [new branch]            win-fma-vnc-apps            -> origin/win-fma-vnc-apps
3801:  * [new branch]            win-mdm-loadtest            -> origin/win-mdm-loadtest
3802:  * [new branch]            windows-baseline            -> origin/windows-baseline
3803:  * [new branch]            windows-end-user-experience-labels -> origin/windows-end-user-experience-labels
3804:  * [new branch]            windows-firefox-doh-script  -> origin/windows-firefox-doh-script
3805:  * [new branch]            windows-mdm-autopilot       -> origin/windows-mdm-autopilot
3806:  * [new branch]            windows-mdm-c-poc           -> origin/windows-mdm-c-poc
3807:  * [new branch]            windows-mdm-docs-auto-enroll -> origin/windows-mdm-docs-auto-enroll
3808:  * [new branch]            windows-mdm-load-test       -> origin/windows-mdm-load-test
3809:  * [new branch]            windows_enable_and_configure-error-message -> origin/windows_enable_and_configure-error-message
3810:  * [new branch]            workstations-to-use-edge-channels -> origin/workstations-to-use-edge-channels
...

3823:  * [new branch]            zayhanlon-patch-1           -> origin/zayhanlon-patch-1
3824:  * [new branch]            zenity-execuser             -> origin/zenity-execuser
3825:  * [new branch]            zhumo-patch-1               -> origin/zhumo-patch-1
3826:  * [new branch]            zhumo-patch-2               -> origin/zhumo-patch-2
3827:  * [new branch]            zhumo-patch-3               -> origin/zhumo-patch-3
3828:  * [new branch]            zhumo-patch-4               -> origin/zhumo-patch-4
3829:  * [new branch]            zhumo-patch-5               -> origin/zhumo-patch-5
3830:  * [new branch]            zhumo-patch-6               -> origin/zhumo-patch-6
3831:  * [new branch]            zhumo-patch-7               -> origin/zhumo-patch-7
3832:  * [new branch]            zwass-actions-reminders     -> origin/zwass-actions-reminders
3833:  * [new branch]            zwass-improve-macos-cpe     -> origin/zwass-improve-macos-cpe
3834:  * [new branch]            zwass-patch-1               -> origin/zwass-patch-1
3835:  * [new branch]            zwass-patch-2               -> origin/zwass-patch-2
3836:  * [new branch]            zwinnerman-fixup            -> origin/zwinnerman-fixup
3837:  * [new branch]            zwinnerman-livequery-tracing -> origin/zwinnerman-livequery-tracing
3838:  * [new branch]            zwinnerman-redis-error-handling -> origin/zwinnerman-redis-error-handling
3839:  * [new branch]            zwinnerman-test             -> origin/zwinnerman-test
...

4528:  check-latest: false
4529:  token: ***
4530:  cache: true
4531:  env:
4532:  GH_TOKEN: ***
4533:  LOG_LEVEL: info
4534:  ##[endgroup]
4535:  Setup go version spec 1.26.4
4536:  Found in cache @ /Users/runner/hostedtoolcache/go/1.26.4/arm64
4537:  Added go to the path
4538:  Successfully set up Go version 1.26.4
4539:  [command]/Users/runner/hostedtoolcache/go/1.26.4/arm64/bin/go env GOMODCACHE
4540:  [command]/Users/runner/hostedtoolcache/go/1.26.4/arm64/bin/go env GOCACHE
4541:  /Users/runner/go/pkg/mod
4542:  /Users/runner/Library/Caches/go-build
4543:  ##[warning]Restore cache failed: Dependencies file is not found in /Users/runner/work/fleet/fleet. Supported file pattern: go.mod
4544:  go version go1.26.4 darwin/arm64
...

4727:  - twingate/darwin
4728:  - ua-connect/darwin
4729:  - viscosity/darwin
4730:  - visual-studio-code/darwin
4731:  - vnc-server/darwin
4732:  - vyprvpn/darwin
4733:  - whatroute/darwin
4734:  - wifiman/darwin
4735:  - windows-app/darwin
4736:  - wireshark-app/darwin
4737:  - workflowy/darwin
4738:  - xcreds/darwin
4739:  - xquartz/darwin
4740:  - zoom-rooms/darwin
4741:  - zoom/darwin
4742:  ##[group]Run # Default to no changes if detection step failed or didn't set output
4743:  �[36;1m# Default to no changes if detection step failed or didn't set output�[0m
4744:  �[36;1mHAS_CHANGES="true"�[0m
...

5063:  go: downloading go.opentelemetry.io/otel v1.43.0
5064:  go: downloading go.opentelemetry.io/otel/metric v1.43.0
5065:  go: downloading go.opentelemetry.io/otel/trace v1.43.0
5066:  go: downloading github.com/mattn/go-colorable v0.1.13
5067:  go: downloading github.com/edsrzf/mmap-go v1.1.0
5068:  go: downloading github.com/secDre4mer/pkcs7 v0.0.0-20240322103146-665324a4461d
5069:  go: downloading go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0
5070:  go: downloading golang.org/x/oauth2 v0.35.0
5071:  go: downloading github.com/aws/aws-sdk-go-v2/config v1.32.12
5072:  go: downloading github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.8
5073:  go: downloading github.com/aws/aws-sdk-go-v2 v1.41.5
5074:  go: downloading github.com/spf13/cobra v1.9.1
5075:  go: downloading github.com/spf13/viper v1.20.1
5076:  go: downloading github.com/go-kit/kit v0.12.0
5077:  go: downloading github.com/smallstep/pkcs7 v0.0.0-20240723090913-5e2c6a136dfa
5078:  go: downloading github.com/hashicorp/go-multierror v1.1.1
5079:  go: downloading github.com/micromdm/plist v0.2.3-0.20260123201933-667adaf87d87
5080:  go: downloading github.com/MicahParks/jwkset v0.11.0
5081:  go: downloading github.com/golang-jwt/jwt/v4 v4.5.2
5082:  go: downloading github.com/WatchBeam/clock v0.0.0-20170901150240-b08e6b4da7ea
5083:  go: downloading github.com/mattn/go-isatty v0.0.20
5084:  go: downloading github.com/andygrunwald/go-jira v1.16.0
5085:  go: downloading github.com/cenkalti/backoff/v4 v4.3.0
5086:  go: downloading github.com/cenkalti/backoff v2.2.1+incompatible
5087:  go: downloading github.com/nukosuke/go-zendesk v0.13.1
5088:  go: downloading go.opentelemetry.io/otel/sdk v1.43.0
5089:  go: downloading github.com/igm/sockjs-go/v3 v3.0.2
5090:  go: downloading gopkg.in/yaml.v2 v2.4.0
5091:  go: downloading github.com/oschwald/maxminddb-golang v1.10.0
5092:  go: downloading golang.org/x/sys v0.45.0
5093:  go: downloading github.com/elastic/go-sysinfo v1.11.2
5094:  go: downloading github.com/pkg/errors v0.9.1
5095:  go: downloading go.elastic.co/fastjson v1.1.0
...

5147:  go: downloading github.com/pelletier/go-toml/v2 v2.2.3
5148:  go: downloading gopkg.in/yaml.v3 v3.0.1
5149:  go: downloading github.com/go-logfmt/logfmt v0.5.1
5150:  go: downloading github.com/groob/finalizer v0.0.0-20170707115354-4c2ed49aabda
5151:  go: downloading cloud.google.com/go/iam v1.5.3
5152:  go: downloading github.com/googleapis/gax-go/v2 v2.17.0
5153:  go: downloading go.opencensus.io v0.24.0
5154:  go: downloading golang.org/x/sync v0.21.0
5155:  go: downloading google.golang.org/grpc v1.79.3
5156:  go: downloading google.golang.org/protobuf v1.36.11
5157:  go: downloading github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8
5158:  go: downloading github.com/nats-io/nkeys v0.4.15
5159:  go: downloading github.com/nats-io/nuid v1.0.1
5160:  go: downloading github.com/micromdm/nanolib v0.2.0
5161:  go: downloading go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352
5162:  go: downloading github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901
5163:  go: downloading cloud.google.com/go/pubsub/v2 v2.0.0
5164:  go: downloading google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7
5165:  go: downloading cloud.google.com/go/auth v0.18.2
5166:  go: downloading google.golang.org/genproto v0.0.0-20260128011058-8636f8732409
5167:  go: downloading cloud.google.com/go/compute/metadata v0.9.0
5168:  go: downloading google.golang.org/genproto/googleapis/api v0.0.0-20260203192932-546029d2fa20
5169:  go: downloading cloud.google.com/go/auth/oauth2adapt v0.2.8
5170:  go: downloading go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0
5171:  go: downloading github.com/google/s2a-go v0.1.9
5172:  go: downloading github.com/googleapis/enterprise-certificate-proxy v0.3.12
5173:  time=level=INFO msg="GOOS environment variable is not set. Using system detected: 'darwin'"
5174:  time=level=INFO msg="INSTALLATION_SEARCH_DIRECTORY environment variable is not set. Using default: '/Applications'"
5175:  time=level=INFO msg="Validating app: AdGuard (adguard/darwin)"
5176:  time=level=INFO msg=Downloading...
5177:  time=level=ERROR msg="Error downloading maintained app: downloading installer: reading installer \"https://static.adguard.com/mac/release/AdGuard-2.18.0.2089.dmg\" contents: context deadline exceeded" app=AdGuard
5178:  time=level=INFO msg="Validating app: AdLock (adlock/darwin)"
5179:  time=level=INFO msg=Downloading...
5180:  time=level=INFO msg="Executing install script..." app=AdLock
5181:  time=level=INFO msg="New application detected at: /Applications/AdLock.app" app=AdLock
5182:  time=level=INFO msg="Forcing LaunchServices refresh for: '/Applications/AdLock.app'" app=AdLock
5183:  time=level=INFO msg="Attempting to remove quarantine for: '/Applications/AdLock.app'" app=AdLock
5184:  time=level=INFO msg="Quarantine output error: checking quarantine status: exit status 1" app=AdLock
5185:  time=level=INFO msg="Quarantine status: Quarantine status: 'xattr: /Applications/AdLock.app: No such xattr: com.apple.quarantine'" app=AdLock
5186:  time=level=INFO msg="Spctl output error: <nil>" app=AdLock
5187:  time=level=INFO msg="spctl status: spctl status: '/Applications/AdLock.app: accepted\nsource=Notarized Developer ID'" app=AdLock
5188:  time=level=WARN msg="Error detected in post-installation steps: Error removing app quarantine: adding app to quarantine exceptions: exit status 4. Attempting to continue" app=AdLock
5189:  time=level=INFO msg="Looking for app: AdLock, version: 2.1.7.3" app=AdLock
5190:  time=level=INFO msg="Found app: 'AdLock' at /Applications/AdLock.app, Version: 2.1.7.3, Bundled Version: 2173" app=AdLock
5191:  time=level=INFO msg="Executing uninstall script for app..." app=AdLock
5192:  time=level=INFO msg="Looking for app: AdLock, version: 2.1.7.3" app=AdLock
5193:  time=level=INFO msg="All checks passed for app: AdLock (adlock/darwin)"
5194:  time=level=INFO msg="Validating app: Adobe Acrobat Reader (adobe-acrobat-reader/darwin)"
5195:  time=level=INFO msg=Downloading...
5196:  time=level=INFO msg="Executing install script..." app="Adobe Acrobat Reader"
5197:  time=level=INFO msg="New application detected at: /Applications/Adobe Acrobat Reader.app" app="Adobe Acrobat Reader"
5198:  time=level=INFO msg="Forcing LaunchServices refresh for: '/Applications/Adobe Acrobat Reader.app'" app="Adobe Acrobat Reader"
5199:  time=level=INFO msg="Attempting to remove quarantine for: '/Applications/Adobe Acrobat Reader.app'" app="Adobe Acrobat Reader"
5200:  time=level=INFO msg="Quarantine output error: checking quarantine status: exit status 1" app="Adobe Acrobat Reader"
5201:  time=level=INFO msg="Quarantine status: Quarantine status: 'xattr: /Applications/Adobe Acrobat Reader.app: No such xattr: com.apple.quarantine'" app="Adobe Acrobat Reader"
5202:  time=level=INFO msg="Spctl output error: <nil>" app="Adobe Acrobat Reader"
5203:  time=level=INFO msg="spctl status: spctl status: '/Applications/Adobe Acrobat Reader.app: accepted\nsource=Notarized Developer ID'" app="Adobe Acrobat Reader"
5204:  time=level=WARN msg="Error detected in post-installation steps: Error removing app quarantine: adding app to quarantine exceptions: exit status 4. Attempting to continue" app="Adobe Acrobat Reader"
5205:  time=level=INFO msg="Looking for app: Adobe Acrobat Reader, version: 26.001.21662" app="Adobe Acrobat Reader"
5206:  time=level=INFO msg="Found app: 'Acrobat Reader' at /Applications/Adobe Acrobat Reader.app, Version: 26.001.21662, Bundled Version: 26.001.21662" app="Adobe Acrobat Reader"
5207:  time=level=INFO msg="Executing uninstall script for app..." app="Adobe Acrobat Reader"
5208:  time=level=INFO msg="Looking for app: Adobe Acrobat Reader, version: 26.001.21662" app="Adobe Acrobat Reader"
5209:  time=level=INFO msg="All checks passed for app: Adobe Acrobat Reader (adobe-acrobat-reader/darwin)"
5210:  time=level=INFO msg="Validating app: AlDente (aldente/darwin)"
5211:  time=level=INFO msg=Downloading...
5212:  time=level=INFO msg="Executing install script..." app=AlDente
5213:  time=level=INFO msg="New application detected at: /Applications/AlDente.app" app=AlDente
5214:  time=level=INFO msg="Forcing LaunchServices refresh for: '/Applications/AlDente.app'" app=AlDente
5215:  time=level=INFO msg="Attempting to remove quarantine for: '/Applications/AlDente.app'" app=AlDente
5216:  time=level=INFO msg="Quarantine output error: checking quarantine status: exit status 1" app=AlDente
5217:  time=level=INFO msg="Quarantine status: Quarantine status: 'xattr: /Applications/AlDente.app: No such xattr: com.apple.quarantine'" app=AlDente
5218:  time=level=INFO msg="Spctl output error: <nil>" app=AlDente
5219:  time=level=INFO msg="spctl status: spctl status: '/Applications/AlDente.app: accepted\nsource=Notarized Developer ID'" app=AlDente
5220:  time=level=WARN msg="Error detected in post-installation steps: Error removing app quarantine: adding app to quarantine exceptions: exit status 4. Attempting to continue" app=AlDente
5221:  time=level=INFO msg="Looking for app: AlDente, version: 1.37.3" app=AlDente
5222:  time=level=INFO msg="Found app: 'AlDente' at /Applications/AlDente.app, Version: 1.37.3, Bundled Version: 94" app=AlDente
5223:  time=level=INFO msg="Executing uninstall script for app..." app=AlDente
5224:  time=level=INFO msg="Looking for app: AlDente, version: 1.37.3" app=AlDente
5225:  time=level=INFO msg="All checks passed for app: AlDente (aldente/darwin)"
5226:  time=level=INFO msg="Validating app: Amazon WorkSpaces (amazon-workspaces/darwin)"
5227:  time=level=INFO msg=Downloading...
5228:  time=level=INFO msg="Executing install script..." app="Amazon WorkSpaces"
5229:  time=level=INFO msg="New application detected at: /Applications/WorkSpaces.app" app="Amazon WorkSpaces"
5230:  time=level=INFO msg="Forcing LaunchServices refresh for: '/Applications/WorkSpaces.app'" app="Amazon WorkSpaces"
5231:  time=level=INFO msg="Attempting to remove quarantine for: '/Applications/WorkSpaces.app'" app="Amazon WorkSpaces"
5232:  time=level=INFO msg="Quarantine output error: checking quarantine status: exit status 1" app="Amazon WorkSpaces"
5233:  time=level=INFO msg="Quarantine status: Quarantine status: 'xattr: /Applications/WorkSpaces.app: No such xattr: com.apple.quarantine'" app="Amazon WorkSpaces"
5234:  time=level=INFO msg="Spctl output error: <nil>" app="Amazon WorkSpaces"
5235:  time=level=INFO msg="spctl status: spctl status: '/Applications/WorkSpaces.app: accepted\nsource=Notarized Developer ID'" app="Amazon WorkSpaces"
5236:  time=level=WARN msg="Error detected in post-installation steps: Error removing app quarantine: adding app to quarantine exceptions: exit status 4. Attempting to continue" app="Amazon WorkSpaces"
5237:  time=level=INFO msg="Looking for app: Amazon WorkSpaces, version: 5.32.0.6080" app="Amazon WorkSpaces"
5238:  time=level=INFO msg="Found app: 'Autoupdate' at /Applications/WorkSpaces.app/Contents/Frameworks/Sparkle.framework/Versions/A/Resources/Autoupdate.app, Version: 1.18.1 21-g558cfd21, Bundled Version: 1.18.1" app="Amazon WorkSpaces"
5239:  time=level=INFO msg="Found app: 'WorkSpaces' at /Applications/WorkSpaces.app, Version: 5.32.0.6080, Bundled Version: 5.32.0.6080" app="Amazon WorkSpaces"
5240:  time=level=INFO msg="Executing uninstall script for app..." app="Amazon WorkSpaces"
5241:  time=level=INFO msg="Looking for app: Amazon WorkSpaces, version: 5.32.0.6080" app="Amazon WorkSpaces"
5242:  time=level=INFO msg="All checks passed for app: Amazon WorkSpaces (amazon-workspaces/darwin)"
5243:  time=level=INFO msg="Validating app: Anka (anka-virtualization/darwin)"
5244:  time=level=INFO msg=Downloading...
5245:  time=level=INFO msg="Executing install script..." app=Anka
5246:  time=level=INFO msg="New application detected at: /Applications/Anka.app" app=Anka
5247:  time=level=INFO msg="Forcing LaunchServices refresh for: '/Applications/Anka.app'" app=Anka
5248:  time=level=INFO msg="Attempting to remove quarantine for: '/Applications/Anka.app'" app=Anka
5249:  time=level=INFO msg="Quarantine output error: checking quarantine status: exit status 1" app=Anka
5250:  time=level=INFO msg="Quarantine status: Quarantine status: 'xattr: /Applications/Anka.app: No such xattr: com.apple.quarantine'" app=Anka
5251:  time=level=INFO msg="Spctl output error: <nil>" app=Anka
5252:  time=level=INFO msg="spctl status: spctl status: '/Applications/Anka.app: accepted\nsource=Notarized Developer ID'" app=Anka
5253:  time=level=WARN msg="Error detected in post-installation steps: Error removing app quarantine: adding app to quarantine exceptions: exit status 4. Attempting to continue" app=Anka
5254:  time=level=INFO msg="Looking for app: Anka, version: 3.9.1" app=Anka
5255:  time=level=INFO msg="Found app: 'Anka' at /Applications/Anka.app, Version: 3.9.1, Bundled Version: 216" app=Anka
5256:  time=level=INFO msg="Executing uninstall script for app..." app=Anka
5257:  time=level=INFO msg="Looking for app: Anka, version: 3.9.1" app=Anka
5258:  time=level=INFO msg="All checks passed for app: Anka (anka-virtualization/darwin)"
5259:  time=level=INFO msg="Validating app: AppCleaner (appcleaner/darwin)"
5260:  time=level=INFO msg=Downloading...
5261:  time=level=INFO msg="Executing install script..." app=AppCleaner
5262:  time=level=INFO msg="New application detected at: /Applications/AppCleaner.app" app=AppCleaner
5263:  time=level=INFO msg="Forcing LaunchServices refresh for: '/Applications/AppCleaner.app'" app=AppCleaner
5264:  time=level=INFO msg="Attempting to remove quarantine for: '/Applications/AppCleaner.app'" app=AppCleaner
5265:  time=level=INFO msg="Quarantine output error: checking quarantine status: exit status 1" app=AppCleaner
5266:  time=level=INFO msg="Quarantine status: Quarantine status: 'xattr: /Applications/AppCleaner.app: No such xattr: com.apple.quarantine'" app=AppCleaner
5267:  time=level=INFO msg="Spctl output error: <nil>" app=AppCleaner
5268:  time=level=INFO msg="spctl status: spctl status: '/Applications/AppCleaner.app: accepted\nsource=Notarized Developer ID'" app=AppCleaner
5269:  time=level=WARN msg="Error detected in post-installation steps: Error removing app quarantine: adding app to quarantine exceptions: exit status 4. Attempting to continue" app=AppCleaner
5270:  time=level=INFO msg="Looking for app: AppCleaner, version: 3.6.8" app=AppCleaner
5271:  time=level=INFO msg="Found app: 'AppCleaner' at /Applications/AppCleaner.app, Version: 3.6.8, Bundled Version: 4332" app=AppCleaner
5272:  time=level=INFO msg="Executing uninstall script for app..." app=AppCleaner
5273:  time=level=INFO msg="Looking for app: AppCleaner, version: 3.6.8" app=AppCleaner
5274:  time=level=INFO msg="All checks passed for app: AppCleaner (appcleaner/darwin)"
5275:  time=level=INFO msg="Validating app: Aviatrix VPN Client (aviatrix-vpn-client/darwin)"
5276:  time=level=INFO msg=Downloading...
5277:  time=level=INFO msg="Executing install script..." app="Aviatrix VPN Client"
5278:  time=level=INFO msg="New application detected at: /Applications/Aviatrix VPN Client.app" app="Aviatrix VPN Client"
5279:  time=level=INFO msg="Forcing LaunchServices refresh for: '/Applications/Aviatrix VPN Client.app'" app="Aviatrix VPN Client"
5280:  time=level=INFO msg="Attempting to remove quarantine for: '/Applications/Aviatrix VPN Client.app'" app="Aviatrix VPN Client"
5281:  time=level=INFO msg="Quarantine output error: checking quarantine status: exit status 1" app="Aviatrix VPN Client"
5282:  time=level=INFO msg="Quarantine status: Quarantine status: 'xattr: /Applications/Aviatrix VPN Client.app: No such xattr: com.apple.quarantine'" app="Aviatrix VPN Client"
5283:  time=level=INFO msg="Spctl output error: <nil>" app="Aviatrix VPN Client"
5284:  time=level=INFO msg="spctl status: spctl status: '/Applications/Aviatrix VPN Client.app: accepted\nsource=Notarized Developer ID'" app="Aviatrix VPN Client"
5285:  time=level=WARN msg="Error detected in post-installation steps: Error removing app quarantine: adding app to quarantine exceptions: exit status 4. Attempting to continue" app="Aviatrix VPN Client"
5286:  time=level=INFO msg="Looking for app: Aviatrix VPN Client, version: 2.17.7" app="Aviatrix VPN Client"
5287:  time=level=INFO msg="Found app: 'Aviatrix VPN Client' at /Applications/Aviatrix VPN Client.app, Version: 2.17.7, Bundled Version: 0.0.0" app="Aviatrix VPN Client"
5288:  time=level=INFO msg="Executing uninstall script for app..." app="Aviatrix VPN Client"
5289:  time=level=INFO msg="Looking for app: Aviatrix VPN Client, version: 2.17.7" app="Aviatrix VPN Client"
5290:  time=level=INFO msg="All checks passed for app: Aviatrix VPN Client (aviatrix-vpn-client/darwin)"
5291:  time=level=INFO msg="Validating app: AWS Client VPN (aws-vpn-client/darwin)"
5292:  time=level=INFO msg=Downloading...
5293:  time=level=INFO msg="Executing install script..." app="AWS Client VPN"
5294:  time=level=INFO msg="New application detected at: /Applications/AWS VPN Client" app="AWS Client VPN"
5295:  time=level=INFO msg="Forcing LaunchServices refresh for: '/Applications/AWS VPN Client'" app="AWS Client VPN"
5296:  time=level=WARN msg="Error detected in post-installation steps: Error forcing LaunchServices refresh: forcing LaunchServices refresh: exit status 1. Attempting to continue" app="AWS Client VPN"
5297:  time=level=INFO msg="Looking for app: AWS Client VPN, version: 5.4.0" app="AWS Client VPN"
5298:  time=level=INFO msg="Found app: 'AWS VPN Client' at /Applications/AWS VPN Client/AWS VPN Client.app, Version: 5.4.0, Bundled Version: 5.4.0" app="AWS Client VPN"
5299:  time=level=INFO msg="Executing uninstall script for app..." app="AWS Client VPN"
5300:  time=level=INFO msg="Looking for app: AWS Client VPN, version: 5.4.0" app="AWS Client VPN"
5301:  time=level=INFO msg="All checks passed for app: AWS Client VPN (aws-vpn-client/darwin)"
5302:  time=level=INFO msg="Validating app: Background Music (background-music/darwin)"
5303:  time=level=INFO msg=Downloading...
5304:  time=level=INFO msg="Executing install script..." app="Background Music"
5305:  time=level=INFO msg="New application detected at: /Applications/Background Music.app" app="Background Music"
5306:  time=level=INFO msg="Forcing LaunchServices refresh for: '/Applications/Background Music.app'" app="Background Music"
5307:  time=level=INFO msg="Attempting to remove quarantine for: '/Applications/Background Music.app'" app="Background Music"
5308:  time=level=INFO msg="Quarantine output error: checking quarantine status: exit status 1" app="Background Music"
5309:  time=level=INFO msg="Quarantine status: Quarantine status: 'xattr: /Applications/Background Music.app: No such xattr: com.apple.quarantine'" app="Background Music"
5310:  time=level=INFO msg="Spctl output error: <nil>" app="Background Music"
5311:  time=level=INFO msg="spctl status: spctl status: '/Applications/Background Music.app: accepted\nsource=Notarized Developer ID'" app="Background Music"
5312:  time=level=WARN msg="Error detected in post-installation steps: Error removing app quarantine: adding app to quarantine exceptions: exit status 4. Attempting to continue" app="Background Music"
5313:  time=level=INFO msg="Looking for app: Background Music, version: 0.5.0" app="Background Music"
5314:  time=level=INFO msg="Found app: 'Background Music' at /Applications/Background Music.app, Version: 0.5.0, Bundled Version: 1.0.0" app="Background Music"
5315:  time=level=INFO msg="Executing uninstall script for app..." app="Background Music"
5316:  time=level=INFO msg="Looking for app: Background Music, version: 0.5.0" app="Background Music"
5317:  time=level=INFO msg="All checks passed for app: Background Music (background-music/darwin)"
5318:  time=level=INFO msg="Validating app: Bartender (bartender/darwin)"
5319:  time=level=INFO msg=Downloading...
5320:  time=level=INFO msg="Executing install script..." app=Bartender
5321:  time=level=INFO msg="New application detected at: /Applications/Bartender 6.app" app=Bartender
5322:  time=level=INFO msg="Forcing LaunchServices refresh for: '/Applications/Bartender 6.app'" app=Bartender
5323:  time=level=INFO msg="Attempting to remove quarantine for: '/Applications/Bartender 6.app'" app=Bartender
5324:  time=level=INFO msg="Quarantine output error: checking quarantine status: exit status 1" app=Bartender
5325:  time=level=INFO msg="Quarantine status: Quarantine status: 'xattr: /Applications/Bartender 6.app: No such xattr: com.apple.quarantine'" app=Bartender
5326:  time=level=INFO msg="Spctl output error: <nil>" app=Bartender
5327:  time=level=INFO msg="spctl status: spctl status: '/Applications/Bartender 6.app: accepted\nsource=Notarized Developer ID'" app=Bartender
5328:  time=level=WARN msg="Error detected in post-installation steps: Error removing app quarantine: adding app to quarantine exceptions: exit status 4. Attempting to continue" app=Bartender
5329:  time=level=INFO msg="Looking for app: Bartender, version: 6.5.2" app=Bartender
5330:  time=level=INFO msg="Found app: 'Bartender 6' at /Applications/Bartender 6.app, Version: 6.5.2, Bundled Version: 652...

@allenhouchins allenhouchins merged commit 1076a51 into main Jul 1, 2026
17 of 20 checks passed
@allenhouchins allenhouchins deleted the fma-2607011802 branch July 1, 2026 22:15
Leanngove pushed a commit that referenced this pull request Jul 2, 2026
Automated ingestion of latest Fleet-maintained app data.

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

* **Bug Fixes**
* Improved uninstall reliability across many macOS apps, especially
where background services use wildcard-style launch service names.
* Uninstall cleanup now more accurately finds and removes matching
services and their related plist/config/log files.
* Added safer handling for cases like “no matching services found” and
more robust app shutdown during uninstall.
* **Updates**
* Updated maintained app definitions and installer/uninstaller script
references, including version refreshes for Dataflare, ElectronMail,
Granola, and JASP (and related Windows installer checksums).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: allenhouchins <32207388+allenhouchins@users.noreply.github.com>
Co-authored-by: Allen Houchins <allenhouchins@mac.com>
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.

2 participants