From f0ba17c1a20968ebe13243ba718ad8b69db59dd4 Mon Sep 17 00:00:00 2001 From: "kilo-code-bot[bot]" <240665456+kilo-code-bot[bot]@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:18:53 -0500 Subject: [PATCH] Add Zotero as fleet-maintained app (#41370) ## Summary - Adds Zotero (reference/research management tool) as a fleet-maintained app with macOS and Windows support. - **macOS**: Uses Homebrew cask `zotero` with DMG installer format (bundle identifier: `org.zotero.zotero`). - **Windows**: Uses WinGet package `DigitalScholar.Zotero` with NSIS (exe) installer, including custom install/uninstall PowerShell scripts with `/S` silent flag. ## Files added | File | Purpose | |------|---------| | `ee/maintained-apps/inputs/homebrew/zotero.json` | macOS input manifest | | `ee/maintained-apps/inputs/winget/zotero.json` | Windows input manifest | | `ee/maintained-apps/inputs/winget/scripts/zotero_install.ps1` | Windows silent install script (NSIS /S) | | `ee/maintained-apps/inputs/winget/scripts/zotero_uninstall.ps1` | Windows silent uninstall script (NSIS /S) | ## Remaining steps (per FMA contributing guide) - [ ] Run `go run cmd/maintained-apps/main.go --slug="zotero/darwin" --debug` to generate macOS output - [ ] Run `go run cmd/maintained-apps/main.go --slug="zotero/windows" --debug` to generate Windows output - [ ] Generate and add app icon using the `tools/software/icons/` script - [ ] Add description to `outputs/apps.json` > **Note:** The WinGet package identifier for Zotero is `DigitalScholar.Zotero` (the community-maintained identifier in the winget-pkgs repository). Built for [Mitch Francese](https://fleetdm.slack.com/archives/D0AG92RJGHY/p1773163983187599?thread_ts=1773163736.129729&cid=D0AG92RJGHY) by [Kilo for Slack](https://kilo.ai/features/slack-integration) --------- Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> Co-authored-by: Mitch Francese <2227948+tux234@users.noreply.github.com> --- .../inputs/homebrew/zotero.json | 8 ++ .../inputs/winget/scripts/zotero_install.ps1 | 28 +++++ .../winget/scripts/zotero_uninstall.ps1 | 98 ++++++++++++++++++ ee/maintained-apps/inputs/winget/zotero.json | 12 +++ ee/maintained-apps/outputs/apps.json | 14 +++ ee/maintained-apps/outputs/zotero/darwin.json | 21 ++++ .../outputs/zotero/windows.json | 21 ++++ .../SoftwarePage/components/icons/Zotero.tsx | 14 +++ .../SoftwarePage/components/icons/index.ts | 2 + .../images/app-icon-zotero-60x60@2x.png | Bin 0 -> 12516 bytes 10 files changed, 218 insertions(+) create mode 100644 ee/maintained-apps/inputs/homebrew/zotero.json create mode 100644 ee/maintained-apps/inputs/winget/scripts/zotero_install.ps1 create mode 100644 ee/maintained-apps/inputs/winget/scripts/zotero_uninstall.ps1 create mode 100644 ee/maintained-apps/inputs/winget/zotero.json create mode 100644 ee/maintained-apps/outputs/zotero/darwin.json create mode 100644 ee/maintained-apps/outputs/zotero/windows.json create mode 100644 frontend/pages/SoftwarePage/components/icons/Zotero.tsx create mode 100644 website/assets/images/app-icon-zotero-60x60@2x.png diff --git a/ee/maintained-apps/inputs/homebrew/zotero.json b/ee/maintained-apps/inputs/homebrew/zotero.json new file mode 100644 index 0000000000..d62e37c7d5 --- /dev/null +++ b/ee/maintained-apps/inputs/homebrew/zotero.json @@ -0,0 +1,8 @@ +{ + "name": "Zotero", + "unique_identifier": "org.zotero.zotero", + "token": "zotero", + "installer_format": "dmg", + "slug": "zotero/darwin", + "default_categories": ["Productivity"] +} diff --git a/ee/maintained-apps/inputs/winget/scripts/zotero_install.ps1 b/ee/maintained-apps/inputs/winget/scripts/zotero_install.ps1 new file mode 100644 index 0000000000..2457e69768 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/zotero_install.ps1 @@ -0,0 +1,28 @@ +# Learn more about .exe install scripts: +# http://fleetdm.com/learn-more-about/exe-install-scripts + +$exeFilePath = "${env:INSTALLER_PATH}" + +try { + +# Add argument to install silently +# NSIS (Nullsoft) installers require /S flag for silent installation +$processOptions = @{ + FilePath = "$exeFilePath" + ArgumentList = "/S" + PassThru = $true + Wait = $true +} + +# Start process and track exit code +$process = Start-Process @processOptions +$exitCode = $process.ExitCode + +# Prints the exit code +Write-Host "Install exit code: $exitCode" +Exit $exitCode + +} catch { + Write-Host "Error: $_" + Exit 1 +} diff --git a/ee/maintained-apps/inputs/winget/scripts/zotero_uninstall.ps1 b/ee/maintained-apps/inputs/winget/scripts/zotero_uninstall.ps1 new file mode 100644 index 0000000000..b09d788485 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/scripts/zotero_uninstall.ps1 @@ -0,0 +1,98 @@ +# Fleet extracts name from installer (EXE) and saves it to PACKAGE_ID +# variable +$softwareName = $PACKAGE_ID + +# It is recommended to use exact software name here if possible to avoid +# uninstalling unintended software. +$softwareNameLike = "*Zotero*" + +# NSIS installers require /S flag for silent uninstall +$uninstallArgs = "/S" + +$paths = @( + 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', + 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall' +) + +$exitCode = 0 + +try { + +[array]$uninstallKeys = Get-ChildItem ` + -Path $paths ` + -ErrorAction SilentlyContinue | + ForEach-Object { Get-ItemProperty $_.PSPath } + +$foundUninstaller = $false +foreach ($key in $uninstallKeys) { + # If needed, add -notlike to the comparison to exclude certain similar + # software + if ($key.DisplayName -like $softwareNameLike) { + $foundUninstaller = $true + # Get the uninstall command. Some uninstallers do not include + # 'QuietUninstallString' and require a flag to run silently. + $uninstallCommand = if ($key.QuietUninstallString) { + $key.QuietUninstallString + } else { + $key.UninstallString + } + + # The uninstall command may contain command and args, like: + # "C:\Program Files\Software\uninstall.exe" /S + # Split the command and args + $splitArgs = $uninstallCommand.Split('"') + if ($splitArgs.Length -gt 1) { + if ($splitArgs.Length -eq 3) { + $existingArgs = $splitArgs[2].Trim() + # Append /S if not already present + if ($existingArgs -notmatch '\b/S\b') { + $uninstallArgs = "$existingArgs /S".Trim() + } else { + $uninstallArgs = $existingArgs + } + } elseif ($splitArgs.Length -gt 3) { + Throw ` + "Uninstall command contains multiple quoted strings. " + + "Please update the uninstall script.`n" + + "Uninstall command: $uninstallCommand" + } + $uninstallCommand = $splitArgs[1] + } else { + # No quotes, check if /S is already in the command + if ($uninstallCommand -notmatch '\b/S\b') { + $uninstallArgs = "/S" + } else { + $uninstallArgs = "" + } + } + Write-Host "Uninstall command: $uninstallCommand" + Write-Host "Uninstall args: $uninstallArgs" + + $processOptions = @{ + FilePath = $uninstallCommand + PassThru = $true + Wait = $true + } + + if ($uninstallArgs -ne '') { + $processOptions.ArgumentList = $uninstallArgs + } + + $process = Start-Process @processOptions + $exitCode = $process.ExitCode + Write-Host "Uninstall exit code: $exitCode" + break + } +} + +if (-not $foundUninstaller) { + Write-Host "Uninstall entry not found for $softwareNameLike" + Exit 0 +} + +Exit $exitCode + +} catch { + Write-Host "Error: $_" + Exit 1 +} diff --git a/ee/maintained-apps/inputs/winget/zotero.json b/ee/maintained-apps/inputs/winget/zotero.json new file mode 100644 index 0000000000..ce05a40cf3 --- /dev/null +++ b/ee/maintained-apps/inputs/winget/zotero.json @@ -0,0 +1,12 @@ +{ + "name": "Zotero", + "slug": "zotero/windows", + "package_identifier": "DigitalScholar.Zotero", + "unique_identifier": "Zotero", + "install_script_path": "ee/maintained-apps/inputs/winget/scripts/zotero_install.ps1", + "uninstall_script_path": "ee/maintained-apps/inputs/winget/scripts/zotero_uninstall.ps1", + "installer_arch": "x64", + "installer_type": "exe", + "installer_scope": "machine", + "default_categories": ["Productivity"] +} diff --git a/ee/maintained-apps/outputs/apps.json b/ee/maintained-apps/outputs/apps.json index b149abb6f6..2ef86101ef 100644 --- a/ee/maintained-apps/outputs/apps.json +++ b/ee/maintained-apps/outputs/apps.json @@ -1813,6 +1813,20 @@ "platform": "windows", "unique_identifier": "Zoom Workplace (X64)", "description": "Zoom is a leading video communication platform for meetings, webinars, and collaboration." + }, + { + "name": "Zotero", + "slug": "zotero/darwin", + "platform": "darwin", + "unique_identifier": "org.zotero.zotero", + "description": "Zotero is a free tool for collecting, organizing, citing, and sharing research." + }, + { + "name": "Zotero", + "slug": "zotero/windows", + "platform": "windows", + "unique_identifier": "Zotero", + "description": "Zotero is a free tool for collecting, organizing, citing, and sharing research." } ] } diff --git a/ee/maintained-apps/outputs/zotero/darwin.json b/ee/maintained-apps/outputs/zotero/darwin.json new file mode 100644 index 0000000000..01345dc09c --- /dev/null +++ b/ee/maintained-apps/outputs/zotero/darwin.json @@ -0,0 +1,21 @@ +{ + "versions": [ + { + "version": "8.0.4", + "queries": { + "exists": "SELECT 1 FROM apps WHERE bundle_identifier = 'org.zotero.zotero';" + }, + "installer_url": "https://download.zotero.org/client/release/8.0.4/Zotero-8.0.4.dmg", + "install_script_ref": "52a11eaf", + "uninstall_script_ref": "24de4c91", + "sha256": "54ef47ec82e9125b80165078565c25d3eca6d5dad190206ef5ab32b8e4493a1a", + "default_categories": [ + "Productivity" + ] + } + ], + "refs": { + "24de4c91": "#!/bin/sh\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/Zotero.app\"\nsudo rmdir '~/Zotero'\ntrash $LOGGED_IN_USER '~/Library/Application Scripts/org.zotero.SafariExtensionApp.SafariExtension'\ntrash $LOGGED_IN_USER '~/Library/Application Support/Zotero'\ntrash $LOGGED_IN_USER '~/Library/Caches/Zotero'\ntrash $LOGGED_IN_USER '~/Library/Containers/org.zotero.SafariExtensionApp.SafariExtension'\ntrash $LOGGED_IN_USER '~/Library/Preferences/org.zotero.zotero.plist'\ntrash $LOGGED_IN_USER '~/Library/Saved Application State/org.zotero.zotero.savedState'\n", + "52a11eaf": "#!/bin/sh\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 if ! osascript -e \"application id \\\"$bundle_id\\\" is running\" 2>/dev/null; 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 [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; 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 [[ $EUID -eq 0 && \"$console_user\" == \"root\" ]]; 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 # Try to launch the application\n if osascript -e \"tell application id \\\"$bundle_id\\\" to activate\" >/dev/null 2>&1; 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 'org.zotero.zotero'\nif [ -d \"$APPDIR/Zotero.app\" ]; then\n\tsudo mv \"$APPDIR/Zotero.app\" \"$TMPDIR/Zotero.app.bkp\"\nfi\nsudo cp -R \"$TMPDIR/Zotero.app\" \"$APPDIR\"\nrelaunch_application 'org.zotero.zotero'\n" + } +} diff --git a/ee/maintained-apps/outputs/zotero/windows.json b/ee/maintained-apps/outputs/zotero/windows.json new file mode 100644 index 0000000000..622a685ebd --- /dev/null +++ b/ee/maintained-apps/outputs/zotero/windows.json @@ -0,0 +1,21 @@ +{ + "versions": [ + { + "version": "8.0.4", + "queries": { + "exists": "SELECT 1 FROM programs WHERE name = 'Zotero' AND publisher = 'Corporation for Digital Scholarship';" + }, + "installer_url": "https://download.zotero.org/client/release/8.0.4/Zotero-8.0.4_x64_setup.exe", + "install_script_ref": "d29b62d2", + "uninstall_script_ref": "599566de", + "sha256": "43632093fc0c59f30d94e312b8b8af2f68d4d529458b0be7bbe1fe629f1edfaa", + "default_categories": [ + "Productivity" + ] + } + ], + "refs": { + "599566de": "# Fleet extracts name from installer (EXE) and saves it to PACKAGE_ID\n# variable\n$softwareName = $PACKAGE_ID\n\n# It is recommended to use exact software name here if possible to avoid\n# uninstalling unintended software.\n$softwareNameLike = \"*Zotero*\"\n\n# NSIS installers require /S flag for silent uninstall\n$uninstallArgs = \"/S\"\n\n$paths = @(\n 'HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall',\n 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall'\n)\n\n$exitCode = 0\n\ntry {\n\n[array]$uninstallKeys = Get-ChildItem `\n -Path $paths `\n -ErrorAction SilentlyContinue |\n ForEach-Object { Get-ItemProperty $_.PSPath }\n\n$foundUninstaller = $false\nforeach ($key in $uninstallKeys) {\n # If needed, add -notlike to the comparison to exclude certain similar\n # software\n if ($key.DisplayName -like $softwareNameLike) {\n $foundUninstaller = $true\n # Get the uninstall command. Some uninstallers do not include\n # 'QuietUninstallString' and require a flag to run silently.\n $uninstallCommand = if ($key.QuietUninstallString) {\n $key.QuietUninstallString\n } else {\n $key.UninstallString\n }\n\n # The uninstall command may contain command and args, like:\n # \"C:\\Program Files\\Software\\uninstall.exe\" /S\n # Split the command and args\n $splitArgs = $uninstallCommand.Split('\"')\n if ($splitArgs.Length -gt 1) {\n if ($splitArgs.Length -eq 3) {\n $existingArgs = $splitArgs[2].Trim()\n # Append /S if not already present\n if ($existingArgs -notmatch '\\b/S\\b') {\n $uninstallArgs = \"$existingArgs /S\".Trim()\n } else {\n $uninstallArgs = $existingArgs\n }\n } elseif ($splitArgs.Length -gt 3) {\n Throw `\n \"Uninstall command contains multiple quoted strings. \" +\n \"Please update the uninstall script.`n\" +\n \"Uninstall command: $uninstallCommand\"\n }\n $uninstallCommand = $splitArgs[1]\n } else {\n # No quotes, check if /S is already in the command\n if ($uninstallCommand -notmatch '\\b/S\\b') {\n $uninstallArgs = \"/S\"\n } else {\n $uninstallArgs = \"\"\n }\n }\n Write-Host \"Uninstall command: $uninstallCommand\"\n Write-Host \"Uninstall args: $uninstallArgs\"\n\n $processOptions = @{\n FilePath = $uninstallCommand\n PassThru = $true\n Wait = $true\n }\n\n if ($uninstallArgs -ne '') {\n $processOptions.ArgumentList = $uninstallArgs\n }\n\n $process = Start-Process @processOptions\n $exitCode = $process.ExitCode\n Write-Host \"Uninstall exit code: $exitCode\"\n break\n }\n}\n\nif (-not $foundUninstaller) {\n Write-Host \"Uninstall entry not found for $softwareNameLike\"\n Exit 0\n}\n\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n", + "d29b62d2": "# Learn more about .exe install scripts:\n# http://fleetdm.com/learn-more-about/exe-install-scripts\n\n$exeFilePath = \"${env:INSTALLER_PATH}\"\n\ntry {\n\n# Add argument to install silently\n# NSIS (Nullsoft) installers require /S flag for silent installation\n$processOptions = @{\n FilePath = \"$exeFilePath\"\n ArgumentList = \"/S\"\n PassThru = $true\n Wait = $true\n}\n \n# Start process and track exit code\n$process = Start-Process @processOptions\n$exitCode = $process.ExitCode\n\n# Prints the exit code\nWrite-Host \"Install exit code: $exitCode\"\nExit $exitCode\n\n} catch {\n Write-Host \"Error: $_\"\n Exit 1\n}\n" + } +} diff --git a/frontend/pages/SoftwarePage/components/icons/Zotero.tsx b/frontend/pages/SoftwarePage/components/icons/Zotero.tsx new file mode 100644 index 0000000000..69164b1871 --- /dev/null +++ b/frontend/pages/SoftwarePage/components/icons/Zotero.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; + +import type { SVGProps } from "react"; + +const Zotero = (props: SVGProps) => ( + + + +); +export default Zotero; diff --git a/frontend/pages/SoftwarePage/components/icons/index.ts b/frontend/pages/SoftwarePage/components/icons/index.ts index f4b5d059e2..6df8aea1f1 100644 --- a/frontend/pages/SoftwarePage/components/icons/index.ts +++ b/frontend/pages/SoftwarePage/components/icons/index.ts @@ -234,6 +234,7 @@ import Zed from "./Zed"; import Zeplin from "./Zeplin"; import ZeroOneZeroEditor from "./010Editor"; import Zoom from "./Zoom"; +import Zotero from "./Zotero"; // SOFTWARE_NAME_TO_ICON_MAP list "special" applications that have a defined // icon for them, keys refer to application names, and are intended to be fuzzy @@ -479,6 +480,7 @@ export const SOFTWARE_NAME_TO_ICON_MAP = { "yubikey manager": YubikeyManager, zed: Zed, zeplin: Zeplin, + zotero: Zotero, } as const; // Maps all known Linux platforms to the LinuxOS icon diff --git a/website/assets/images/app-icon-zotero-60x60@2x.png b/website/assets/images/app-icon-zotero-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..52c7b37f505cea6d73794299a0e55afdeec38fa6 GIT binary patch literal 12516 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 P5Bn}7sVGq+ZWQ=m74zhK literal 0 HcmV?d00001