Foreword¶
In DSH’s plugin ecosystem, there are many runnable plugins, but few that can act on the local environment for you. Agents often need to know “which applications are installed on this machine and if there are new versions,” but common approaches are either letting the model concatenate winget upgrade commands or writing a script specific to a single piece of software. The problem with these approaches is that they provide incomplete lists, don’t recognize architecture, don’t know silent parameters, and once executed, they install directly with current process permissions.
Introducing dsh-app-updater below. It puts the pipeline of “List installed apps → Compare new versions → Download full installer from official website → Install via official silent parameters” into a single DSH plugin. The entry point is in the settings interface; it doesn’t require the command line to use.
What is it¶
dsh-app-updater, author ccgo26, MIT license, current package version 1.3.15, categorized under “Networking Tools” in the Community Plugins directory, 1 star on GitHub.
One-sentence positioning: It is DSH’s application updater plugin that detects if any of the machine’s all installed applications have new versions and silently updates them in one click by downloading the complete installers from their respective official websites. The detection process doesn’t require opening the software being detected; the plugin automatically identifies Windows / macOS and ARM64 / x64 (Intel) architectures, and selects the corresponding installer based on the current system and chip.
Let’s clarify the known limitations first: On Windows, it only drives apps indexed by the winget directory, and uses the registry to supplement unlisted ones; on macOS, it only provides one-click updates for apps managed by Homebrew. Apps installed by dragging DMGs can be listed and checked for new versions on the official website, but cannot be updated in one click.
Core Features¶
Four steps, clearly written in the README:
- List installed applications: Windows uses
winget list(covering software in thewingetdirectory + Store/MSIX packages), then uses threeUninstallkeys in the Windows registry to supplement software not indexed bywinget. The whole process is read-only for files and the registry, and does not launch any applications. - Check for new versions:
winget upgrade --include-unknown, providing “installed version → upgradeable version” from the officialwingetsoftware directory, then merging with the list from Step 1 by ID. - One-click update:
winget downloaddownloads the full installer from the publisher’s official website to a specified directory (verifying SHA-256 against the list during download); before execution, the plugin re-verifies the hash locally (since 1.3.5); if inconsistent, it doesn’t execute and falls back to a backup method; then it parses thewingetmanifest that comes with the package, extracts the silent parameters declared by the publisher, and executes the silent installation; after installation, it useswinget list --idto verify the version number. - Failure fallback: If the official website installer is re-issued by the publisher causing hash inconsistencies with the directory record, or if downloading is prohibited by the manifest, it automatically falls back to
winget upgrade --id <id> --exact --silent—still downloading the full installer from the official website, but leaving the download and installation process towinget.
The point of “downloading from the corresponding official website” is worth mentioning separately: Installer addresses come from InstallerUrl in the winget manifest, for example, Git points to https://github.com/git-for-windows/git/releases/download/..., CPU-Z points to https://download.cpuid.com/cpu-z/..., Bilibili points to https://dl.hdslb.com/..., Chrome points to https://dl.google.com/dl/chrome/install/.... The download directory (default %USERPROFILE%\Downloads\DSH App Updater) will also retain a .yaml manifest inside, containing the InstallerUrl / InstallerSha256 / InstallerSwitches.Silent declared by the publisher, which you can verify yourself.
The logic on the macOS side is different and easier to misinterpret. Applications are enumerated from directories like /Applications, then checked against the cask directory to see if they are installed via Homebrew; apps installed by dragging DMGs or manually are listed truthfully and marked “Not managed by Homebrew” and will not be counted as updatable. Therefore, on a Mac with only drag-and-drop apps, the interface showing “0 updatable” is the correct conclusion, not a bug.
However, since 1.3.9, non-Homebrew apps can also check for the “latest official version” because macOS apps carry update sources themselves:
| Type | Location | What to read |
|---|---|---|
| Sparkle | Info.plist’s SUFeedURL → appcast.xml |
<sparkle:shortVersionString> (not build number) |
| Electron | Contents/Resources/app-update.yml |
url + channel → grab the top-level version: of the corresponding mac install manifest |
After finding it, the interface displays the official version in the “Can upgrade to” column and marks it with an orange “Manual required” tag, the status column shows “New version available on official site,” and the status bar adds a sentence: “N other apps have new versions on official site, manual upgrade required.” This path is for display only: hasUpdate / updateSupported are still determined solely by Homebrew, and finding a version doesn’t mean it can upgrade for you. The query process is read-only; no downloading, installing, or launching of the detected applications.
Since 1.3.10, it has been relaxed slightly: non-Homebrew apps that match a cask can be directly updated in one click, executing:
brew install --cask --force <token>
It will remove the existing .app and install the cask version; after that, the app automatically becomes “Homebrew managed,” and subsequent updates will follow the normal brew upgrade --cask. The README explains why it’s not --adopt: Homebrew’s --adopt only accepts when the existing app is identical to the version to install; if the versions differ, it throws an error directly, so it can only “claim an app that is already up to date” and cannot be used for upgrades. Apps that don’t match a cask still cannot be updated; the interface shows “Manual required” and prompts to upgrade in the app or on the website; since 1.3.8, the prompt for these rows will directly write out the corresponding command, e.g., brew install --cask --adopt iina.
Installation and Enablement¶
Installation is done via dsh plugin, recommended to install from npm, and can also be installed in one click via the plugin center:
dsh plugin --profile web add dsh-app-updater
Newly released versions might be temporarily held back by pnpm’s minimumReleaseAge strategy; to install a specific version immediately, explicitly write the version number:
dsh plugin --profile web add dsh-app-updater@1.2.0
If you don’t want to use npm, you can also use the source repository or pre-built packages:
# Source repository
dsh plugin --profile web add github:ccgo26/dsh-app-updater
# Pre-built tgz (see Releases)
dsh plugin --profile web add https://github.com/ccgo26/dsh-app-updater/releases/latest/download/dsh-app-updater.tgz
Dependency requirements: Node ^22.19.0 || >=24.0.0, DSH >=0.1.5-rc.1 <0.2.0-0 || >=0.2.0-rc.2 <0.3.0-0.
The desktop client (Electron-hosted desktop profile) does not allow direct overwriting by dsh plugin; you need to create a junction under the profile’s node_modules and manually supplement bundles. After installation, restart DSH; the entry point is Settings → App Updater.
Typical Usage¶
The entire interaction flow is within the settings page; there are no background tasks:
- Open Settings → App Updater.
- Click Refresh Check. The plugin doesn’t auto-scan; it only scans when you click.
- The list is sorted by “New version priority” by default, but can be filtered using the search box and All / Only updatable / Unlisted / Manual required.
- Check the apps to update, or click Update All to process all updatable items under the current filter; clicking Update at the end of a row updates individually.
- During update, there is a progress bar, current app name, and per-line logs; Cancel stops projects that haven’t started yet.
- Open Download Directory allows you to view the downloaded official installers in File Explorer.
- In the Plugin Settings panel on the same page, you can change the download directory, keep installers, dry-run mode switches, etc.
- Table column widths are set to fully display the app name by default; dragging the divider on the right of the header adjusts the width; double-click restores defaults; preferences are saved locally in the browser.
The meaning of the status column needs to be distinguished: Has new version means the software directory provides a higher version number; Up to date means there is no higher version in the directory; Unlisted means the directory found an update but this plugin cannot drive it (possible reasons: software directory not indexed, managed by app store, unknown version, or system component). When a red warning banner appears, it indicates that the judgment basis for this scan itself failed (Homebrew command failed, or cask directory couldn’t be fetched). At this point, “0 updatable” does not mean all apps are up to date.
Changing Settings via API¶
Settings panel actions can also be done via the HTTP interface, suitable for scripting:
curl -X POST -H "Content-Type: application/json" \
-d '{"confirm":true,"settings":{"dryRun":true}}' \
http://127.0.0.1:<port>/dsh-app-updater/settings
downloadDir is validated: it must be empty (use default directory) or an absolute path (D:\Apps) / UNC path (\\server\share), otherwise return 400, show red text in the panel, and won’t write installers to unexpected locations.
Settings¶
Status file: %USERPROFILE%\.dsh\dsh-app-updater\state.json
| Key | Default | Description |
|---|---|---|
includeStoreApps |
true |
Whether to include store apps (Windows is MSIX, macOS is App Store apps; display only, not updated). |
includeRegistryOnly |
true |
Windows: Whether to use the registry to supplement apps not indexed by winget (display only, cannot update). |
includeUnmanagedApps |
true |
macOS: Whether to list non-Homebrew managed apps (display only, cannot update). |
checkOfficialSources |
true |
macOS: Whether to read update sources built into non-Homebrew apps to check if there is a new official version. Read-only; when turned off, these apps show “Cannot judge,” and the scan no longer initiates requests to these domains. |
downloadDir |
Empty | Custom installer download directory; empty = %USERPROFILE%\Downloads\DSH App Updater (macOS is ~/Downloads/DSH App Updater). |
keepInstallers |
true |
Keep installers after update; if false, delete the just-downloaded package for that app after installation completes. |
hashFallbackUpgrade |
true |
Fallback to winget upgrade when download fails. |
dryRun |
false |
Dry Run Mode: Only download the full official installer, do not execute installation. Used to see what will be installed first. |
matchSystemArch |
true |
Select installer based on detected system and chip architecture (Windows passes --architecture; macOS is decided by Homebrew). |
autoElevate |
true |
Auto-Elevate Installation: Installers requiring admin rights are executed in an elevated context; a confirmation prompt appears each time if UAC is on. |
Security Boundaries¶
The plugin downloads and installs executable files, so the security constraints are written in detail and worth a separate section.
- Route default deny: Every request first goes through
ctx.connection.admit(request), which is the boundary DSH uses for Host/Origin trust verification and browser session / API authentication (the host’s own/apiroutes use it). Requests that fail get401/403/409directly, withX-DSH-Admission: deniedin the response header. - Compatible mode default off (since 1.3.1): When the host lacks a
connectionservice, it no longer “allows solely based on loopback address,” always returning403. If needed, it must be explicitly enabled, one of two options: overrideallowLegacyLoopback: trueincordis.patch.yml, or set the environment variableDSH_APP_UPDATER_ALLOW_LEGACY_LOOPBACK=1before starting DSH. When enabled, allowed loopback requests carryX-DSH-Admission: loopback-legacy-optinand warning headers, a red warning banner appears at the top of the settings page, and awarnlog is recorded. - Source Fencing (since 1.3.5): In compatible mode, cross-site browser requests are rejected. Requests with
Origin/Refererexisting and different source from the request’sHost, or withSec-Fetch-Site: cross-site, all get403. Browsers block scripts from forging these headers, sofetch(…/update)without cookies in a third-party page cannot drive “download + elevate install”; local scripts and host IPC bridges are unaffected. - Explicit confirmation for write operations:
scan/update/cancel/settings/open-downloadsmust includeconfirm:true. - Don’t guess silent parameters: Prioritize the
Silentparameter declared by the publisher in the manifest; only use conservative default switches based on installer type if the manifest is missing. - Local SHA-256 verification before execution (since 1.3.5): After
winget downloadcompletes, re-calculate the hash of the streamed installer against theInstallerSha256in the manifest accompanying the package before execution—this file might run as administrator immediately after, so it must be blocked if replaced or corrupted mid-process; if inconsistent, it doesn’t execute and falls back to a backup method. If the hash matching the current architecture isn’t found in the manifest, skip and record it; never misjudge by using a hash from a different architecture. - Auto-elevate when admin is required: Machine-level installers (MSI, writing to
Program Files) are handed to the elevated copy ofStart-Process -Verb RunAs, which can be turned off in settings.
Applicable Scenarios and Notes¶
Suitable for developers who often need to confirm if software versions on a dev machine or jumpbox are outdated, or want to make “check update + install update” a reproducible step without introducing an extra update daemon. Windows side depends on winget being available, macOS side depends on Homebrew being available; the plugin itself does not replace these two package managers.
Note three points:
- The plugin runs with the current dsh process permissions. When using it to batch update system components or machine-level app packages, the elevated copy is actually executed, and UAC will pop confirmations one by one; turn off
autoElevateif you don’t want to see confirmations. - You should check the source code and license before installing. This plugin is MIT licensed, repo at
github.com/ccgo26/dsh-app-updater; it downloads installers from the publisher’s official website, and security boundaries depend on the trustworthiness of the manifest in thewingetdirectory. Default is not to enable compatible mode, and write operations requireconfirm:true; it is recommended to keep these two defaults. - Official version check for non-Homebrew apps is read-only and can be toggled globally (
checkOfficialSources). When turned off, these apps return to “Cannot judge,” and the scan no longer initiates requests to relevant domains—useful for dev environments with unexpected network connections.
Conclusion¶
dsh-app-updater solves a specific problem: serializing the pipeline of “detect local app versions → get full installer from official website → install via official silent parameters → verify version number” into an auditable chain, and adding a local hash verification step after downloading and before execution. It is not a replacement for package managers, but rather organizes the capabilities of winget and Homebrew into a DSH plugin with a UI, status, and security boundaries. Functionality is focused on the two paths: Windows winget and macOS Homebrew. Non-Homebrew apps can be listed and have their versions checked, but upgrades are either absorbed into Homebrew or handled manually.
Directory page: https://www.skillhub.cn/plugins/ccgo26/dsh-app-updater
Repository: https://github.com/ccgo26/dsh-app-updater