Skip to content

Add Joplin as a macOS and Windows FMA - #46424

Merged
allenhouchins merged 1 commit into
mainfrom
fma-joplin
May 29, 2026
Merged

Add Joplin as a macOS and Windows FMA#46424
allenhouchins merged 1 commit into
mainfrom
fma-joplin

Conversation

@harrisonravazzolo

@harrisonravazzolo harrisonravazzolo commented May 29, 2026

Copy link
Copy Markdown
Member

Related issue: Resolves #

Checklist for submitter

If some of the following don't apply, delete the relevant line.

  • Changes file added for user-visible changes in changes/, orbit/changes/ or ee/fleetd-chrome/changes.
    See Changes files for more information.

  • Input data is properly validated, SELECT * is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.

  • Timeouts are implemented and retries are limited to avoid infinite loops

  • If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes

Testing

For unreleased bug fixes in a release candidate, one of:

  • Confirmed that the fix is not expected to adversely impact load test results
  • Alerted the release DRI if additional load testing is needed

Database migrations

  • Checked schema for all modified table for columns that will auto-update timestamps during migration.
  • Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects.
  • Ensured the correct collation is explicitly set for character columns (COLLATE utf8mb4_unicode_ci).

New Fleet configuration settings

  • Setting(s) is/are explicitly excluded from GitOps

If you didn't check the box above, follow this checklist for GitOps-enabled settings:

  • Verified that the setting is exported via fleetctl generate-gitops
  • Verified the setting is documented in a separate PR to the GitOps documentation
  • Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional)
  • Verified that any relevant UI is disabled when GitOps mode is enabled

fleetd/orbit/Fleet Desktop

  • Verified compatibility with the latest released version of Fleet (see Must rule)
  • If the change applies to only one platform, confirmed that runtime.GOOS is used as needed to isolate changes
  • Verified that fleetd runs on macOS, Linux and Windows
  • Verified auto-update works from the released version of component to the new version (see tools/tuf/test)

Summary by CodeRabbit

  • New Features
    • Added support for Joplin application installation and management on macOS via Homebrew.
    • Added support for Joplin application installation and management on Windows via Winget.
    • Integrated Joplin icon into the application interface for visual identification.

Review Change Stack

@codecov

codecov Bot commented May 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 66.88%. Comparing base (8ef2590) to head (4142997).
⚠️ Report is 11 commits behind head on main.

Files with missing lines Patch % Lines
...end/pages/SoftwarePage/components/icons/Joplin.tsx 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #46424      +/-   ##
==========================================
- Coverage   66.88%   66.88%   -0.01%     
==========================================
  Files        2791     2792       +1     
  Lines      222293   222295       +2     
  Branches    11469    11334     -135     
==========================================
+ Hits       148682   148683       +1     
- Misses      60161    60163       +2     
+ Partials    13450    13449       -1     
Flag Coverage Δ
frontend 56.30% <50.00%> (-0.01%) ⬇️

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

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

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

@harrisonravazzolo harrisonravazzolo changed the title joplin for macos and win FMA - Joplin for macOS and Windows May 29, 2026
@harrisonravazzolo
harrisonravazzolo marked this pull request as ready for review May 29, 2026 06:32
@harrisonravazzolo
harrisonravazzolo requested a review from a team as a code owner May 29, 2026 06:32

@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 May 29, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR adds Joplin as a maintained app with cross-platform support. It introduces Homebrew input configuration and macOS/darwin deployment scripts for installation, Winget input configuration with PowerShell install/uninstall scripts for Windows, registers both platform variants in the app catalog, and adds a frontend icon component with UI integration.

Possibly related PRs

  • fleetdm/fleet#46264: Extends maintained-app integration by adding app entries to the app catalog and registering a new icon in SOFTWARE_NAME_TO_ICON_MAP.
  • fleetdm/fleet#46284: Follows the same pattern of adding a new maintained app with app catalog registration and icon component integration.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description uses the template but all checklist items remain unchecked with no explanatory text about which items apply or why they were not completed. Complete the description by checking applicable checklist items and removing non-applicable ones; add explanations for key decisions like security validation, testing, and platform verification.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title accurately summarizes the main change: adding Joplin as a Flexible Mobile App (FMA) for both macOS and Windows platforms.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fma-joplin

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 and usage tips.

@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: 2

🧹 Nitpick comments (1)
frontend/pages/SoftwarePage/components/icons/Joplin.tsx (1)

5-13: ⚡ Quick win

Consider converting the base64 PNG to SVG vector paths.

The current implementation embeds a base64-encoded PNG (~3.5KB) inside the SVG. This approach has drawbacks compared to native SVG paths:

  • Not resolution-independent (may pixelate when scaled)
  • Larger bundle size
  • Cannot be styled/themed via CSS

If the Joplin logo can be traced to vector paths, it would improve scalability and reduce bundle size.

🤖 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 `@frontend/pages/SoftwarePage/components/icons/Joplin.tsx` around lines 5 - 13,
The Joplin component currently embeds a base64 PNG via an <image> tag; replace
that raster blob by importing or inlining the Joplin vector markup and swapping
the <image> element for native SVG path/shape elements inside the Joplin
function, keep the outer <svg ... {...props}> and width/height/viewBox behavior,
optimize the vector with an SVG optimizer (SVGO) to reduce size, ensure
colorable parts use currentColor or CSS-friendly attributes so the icon can be
themed, and verify scaling and visual parity across typical sizes after the
change.
🤖 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/joplin/darwin.json`:
- Line 20: The script currently deletes user profile data by calling trash
$LOGGED_IN_USER '~/Library/Application Support/Joplin' inside the trash() flow;
remove that destructive call and instead only remove the app bundle (sudo rm -rf
"$APPDIR/Joplin.app") and non-user-data artifacts like caches or helper prefs
(e.g., ~/Library/Caches/net.cozic.joplin-desktop and the prefs/helper plist
lines already present). If retaining an option to purge user data is required,
implement it behind an explicit opt-in flag or documented prompt rather than
unconditionally invoking trash on '~/Library/Application Support/Joplin'.
- Line 19: The install script currently moves existing APPDIR/Joplin.app to
TMPDIR/Joplin.app.bkp before verifying the new copy, risking leaving the app
missing on failure; update the flow in the mounting/copy block to: copy the new
Joplin.app from MOUNT_POINT to a TMPDIR staging path (e.g.,
TMPDIR/Joplin.app.new), verify the copy completed and is a valid .app (check
existence and a basic expected file inside), then atomically replace the live
app by renaming/moving the old APPDIR/Joplin.app to a backup name and moving
TMPDIR/Joplin.app.new into APPDIR (and if any step fails restore the backup into
APPDIR), ensure permissions are preserved and cleanup TMPDIR and MOUNT_POINT;
reference functions/vars quit_and_track_application, relaunch_application,
APPDIR, TMPDIR, MOUNT_POINT to locate where to implement these checks and
rollback behavior.

---

Nitpick comments:
In `@frontend/pages/SoftwarePage/components/icons/Joplin.tsx`:
- Around line 5-13: The Joplin component currently embeds a base64 PNG via an
<image> tag; replace that raster blob by importing or inlining the Joplin vector
markup and swapping the <image> element for native SVG path/shape elements
inside the Joplin function, keep the outer <svg ... {...props}> and
width/height/viewBox behavior, optimize the vector with an SVG optimizer (SVGO)
to reduce size, ensure colorable parts use currentColor or CSS-friendly
attributes so the icon can be themed, and verify scaling and visual parity
across typical sizes after the change.
🪄 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: d147ef89-d52e-4fa1-a3d7-b375e87eabde

📥 Commits

Reviewing files that changed from the base of the PR and between 8ef2590 and 4142997.

⛔ Files ignored due to path filters (1)
  • website/assets/images/app-icon-joplin-60x60@2x.png is excluded by !**/*.png
📒 Files selected for processing (9)
  • ee/maintained-apps/inputs/homebrew/joplin.json
  • ee/maintained-apps/inputs/winget/joplin.json
  • ee/maintained-apps/inputs/winget/scripts/joplin_install.ps1
  • ee/maintained-apps/inputs/winget/scripts/joplin_uninstall.ps1
  • ee/maintained-apps/outputs/apps.json
  • ee/maintained-apps/outputs/joplin/darwin.json
  • ee/maintained-apps/outputs/joplin/windows.json
  • frontend/pages/SoftwarePage/components/icons/Joplin.tsx
  • frontend/pages/SoftwarePage/components/icons/index.ts

}
],
"refs": {
"1e261b4b": "#!/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)\nhdiutil attach -plist -nobrowse -readonly -mountpoint \"$MOUNT_POINT\" \"$INSTALLER_PATH\"\nsudo cp -R \"$MOUNT_POINT\"/* \"$TMPDIR\"\nhdiutil detach \"$MOUNT_POINT\"\n# copy to the applications folder\nquit_and_track_application 'net.cozic.joplin-desktop'\nif [ -d \"$APPDIR/Joplin.app\" ]; then\n\tsudo mv \"$APPDIR/Joplin.app\" \"$TMPDIR/Joplin.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Joplin.app\" \"$APPDIR\"\nrelaunch_application 'net.cozic.joplin-desktop'\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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the app replacement rollback-safe.

The install script moves the live /Applications/Joplin.app to a backup before the new copy is proven good. If the DMG extraction or final copy fails, the host is left without Joplin and the backup is never restored.

Suggested hardening
-if [ -d "$APPDIR/Joplin.app" ]; then
-	sudo mv "$APPDIR/Joplin.app" "$TMPDIR/Joplin.app.bkp"
-fi
-sudo cp -R "$TMPDIR/Joplin.app" "$APPDIR"
+sudo rm -rf "$TMPDIR/Joplin.app.new"
+sudo cp -R "$TMPDIR/Joplin.app" "$TMPDIR/Joplin.app.new"
+
+if [ -d "$APPDIR/Joplin.app" ]; then
+	sudo mv "$APPDIR/Joplin.app" "$TMPDIR/Joplin.app.bkp"
+fi
+
+if sudo mv "$TMPDIR/Joplin.app.new" "$APPDIR/Joplin.app"; then
+	sudo rm -rf "$TMPDIR/Joplin.app.bkp"
+elif [ -d "$TMPDIR/Joplin.app.bkp" ]; then
+	sudo mv "$TMPDIR/Joplin.app.bkp" "$APPDIR/Joplin.app"
+	exit 1
+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
"1e261b4b": "#!/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)\nhdiutil attach -plist -nobrowse -readonly -mountpoint \"$MOUNT_POINT\" \"$INSTALLER_PATH\"\nsudo cp -R \"$MOUNT_POINT\"/* \"$TMPDIR\"\nhdiutil detach \"$MOUNT_POINT\"\n# copy to the applications folder\nquit_and_track_application 'net.cozic.joplin-desktop'\nif [ -d \"$APPDIR/Joplin.app\" ]; then\n\tsudo mv \"$APPDIR/Joplin.app\" \"$TMPDIR/Joplin.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Joplin.app\" \"$APPDIR\"\nrelaunch_application 'net.cozic.joplin-desktop'\n",
"1e261b4b": "#!/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)\nhdiutil attach -plist -nobrowse -readonly -mountpoint \"$MOUNT_POINT\" \"$INSTALLER_PATH\"\nsudo cp -R \"$MOUNT_POINT\"/* \"$TMPDIR\"\nhdiutil detach \"$MOUNT_POINT\"\n# copy to the applications folder\nquit_and_track_application 'net.cozic.joplin-desktop'\nsudo rm -rf \"$TMPDIR/Joplin.app.new\"\nsudo cp -R \"$TMPDIR/Joplin.app\" \"$TMPDIR/Joplin.app.new\"\n\nif [ -d \"$APPDIR/Joplin.app\" ]; then\n\tsudo mv \"$APPDIR/Joplin.app\" \"$TMPDIR/Joplin.app.bkp\"\nfi\n\nif sudo mv \"$TMPDIR/Joplin.app.new\" \"$APPDIR/Joplin.app\"; then\n\tsudo rm -rf \"$TMPDIR/Joplin.app.bkp\"\nelif [ -d \"$TMPDIR/Joplin.app.bkp\" ]; then\n\tsudo mv \"$TMPDIR/Joplin.app.bkp\" \"$APPDIR/Joplin.app\"\n\texit 1\nfi\nrelaunch_application 'net.cozic.joplin-desktop'\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/joplin/darwin.json` at line 19, The install script
currently moves existing APPDIR/Joplin.app to TMPDIR/Joplin.app.bkp before
verifying the new copy, risking leaving the app missing on failure; update the
flow in the mounting/copy block to: copy the new Joplin.app from MOUNT_POINT to
a TMPDIR staging path (e.g., TMPDIR/Joplin.app.new), verify the copy completed
and is a valid .app (check existence and a basic expected file inside), then
atomically replace the live app by renaming/moving the old APPDIR/Joplin.app to
a backup name and moving TMPDIR/Joplin.app.new into APPDIR (and if any step
fails restore the backup into APPDIR), ensure permissions are preserved and
cleanup TMPDIR and MOUNT_POINT; reference functions/vars
quit_and_track_application, relaunch_application, APPDIR, TMPDIR, MOUNT_POINT to
locate where to implement these checks and rollback behavior.

],
"refs": {
"1e261b4b": "#!/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)\nhdiutil attach -plist -nobrowse -readonly -mountpoint \"$MOUNT_POINT\" \"$INSTALLER_PATH\"\nsudo cp -R \"$MOUNT_POINT\"/* \"$TMPDIR\"\nhdiutil detach \"$MOUNT_POINT\"\n# copy to the applications folder\nquit_and_track_application 'net.cozic.joplin-desktop'\nif [ -d \"$APPDIR/Joplin.app\" ]; then\n\tsudo mv \"$APPDIR/Joplin.app\" \"$TMPDIR/Joplin.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Joplin.app\" \"$APPDIR\"\nrelaunch_application 'net.cozic.joplin-desktop'\n",
"48ef0ce5": "#!/bin/bash\n\n# variables\nAPPDIR=\"/Applications/\"\nLOGGED_IN_USER=$(scutil <<< \"show State:/Users/ConsoleUser\" | awk '/Name :/ { print $3 }')\n# functions\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 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\nsudo rm -rf \"$APPDIR/Joplin.app\"\ntrash $LOGGED_IN_USER '~/Library/Application Support/Joplin'\ntrash $LOGGED_IN_USER '~/Library/Preferences/net.cozic.joplin-desktop.helper.plist'\ntrash $LOGGED_IN_USER '~/Library/Preferences/net.cozic.joplin-desktop.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/net.cozic.joplin-desktop.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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't wipe the user's Joplin profile on uninstall.

Removing ~/Library/Application Support/Joplin makes uninstall destructive: that path is the user's app support/profile directory, not just disposable binaries. For a notes app, this risks deleting notebooks and local state during a normal uninstall flow.

🤖 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/joplin/darwin.json` at line 20, The script
currently deletes user profile data by calling trash $LOGGED_IN_USER
'~/Library/Application Support/Joplin' inside the trash() flow; remove that
destructive call and instead only remove the app bundle (sudo rm -rf
"$APPDIR/Joplin.app") and non-user-data artifacts like caches or helper prefs
(e.g., ~/Library/Caches/net.cozic.joplin-desktop and the prefs/helper plist
lines already present). If retaining an option to purge user data is required,
implement it behind an explicit opt-in flag or documented prompt rather than
unconditionally invoking trash on '~/Library/Application Support/Joplin'.

@allenhouchins allenhouchins changed the title FMA - Joplin for macOS and Windows Add Joplin as a macOS and Windows FMA May 29, 2026
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.

3 participants