Introduction¶
The philosophy of DSH is “everything is a plugin,” and where a plugin is installed on a host and to which version depends on manual maintenance on each machine. When you have desktops, servers, or a few development machines, you either have to manually align them one by one, or maintain a package manifest repository that everyone can read. The former is labor-intensive, while the latter involves version comparison, conflict handling, and additional management of repository read/write permissions.
The approach taken by dsh-plugin-syncup to solve this problem is: the manifest itself does not go into the version repository but is stored in a private GitHub Gist, with the plugin responsible for pushing and pulling. It is maintained by jijijitian, with the source code on GitHub; it is currently a small project with 2 stars. Below is an introduction to its positioning, installation, and typical usage.
What is this¶
Positioning in one sentence: This is a DSH plugin used to write the local profile bundle manifest to a secret GitHub Gist and read the same manifest back from other hosts, previewing conflicts before you decide whether to apply them.
It performs “manifest synchronization,” not “configuration synchronization” or “code synchronization.” The synchronization target is only the plugin manifest itself (which bundles are enabled, what version, what source), and an optional refresh of plugin versions.
A few clear boundaries:
- Starting from 2.0.0, manual synchronization is the only method; automatic synchronization, scheduled synchronization, or automatic synchronization triggered by plugin changes are no longer supported.
- Starting from 2.2.0, manifest storage moved from GitHub repositories to private Gists.
- Starting from 2.4.0, look up by Gist name, defaulting to
dsh-plugin-sync, automatically creating it on the first push if it doesn’t exist. - Current version 4.0.0, targeting DSH
0.2.1-rc.3plugin page configuration slotplugins.bundle.config. - The plugin itself has no npm runtime dependencies, and the license is MIT.
Core Capabilities¶
Capabilities verifiable from the README:
- Modify the configuration of this plugin directly in the DSH plugin page without needing to modify code.
- On the first click of “Create Gist and Push,” the plugin automatically creates a private Gist.
- Other hosts only need to fill in the same Gist name; they don’t need to create a repository or a Gist themselves.
- Clicking “Pull Online Manifest” only reads and previews; it does not install anything.
- When you need to upload the local manifest, manually click “Push Local Manifest.”
In addition, there is a separate “Plugin Update” tab that bypasses the manifest to ask npm and GitHub for the real latest versions, allowing you to select the installation channel item by item. This part will be expanded upon separately later.
Installation and Enablement¶
The installation command comes from the README:
plugin_manager action=install_bundle target=github:jijijitian/dsh-plugin-syncup
After installation, open Plugins → Installed Plugins → dsh-plugin-syncup to see the configuration and manual synchronization panel. Note that the plugin must be installed once on every host; it does not automatically install itself.
If upgrading from 1.x or enabling the page for the first time, restart DSH. The reason is that DSH’s client-modules cache package metadata based on Loader entries until the process restarts; installing 1.0 first (without dsh.client) and then 2.x will not immediately show the configuration panel; it will only be detected after restarting once to scan the new client half. After that, a normal page refresh suffices.
GitHub Token¶
Gist requires a GitHub token; this is the only credential the plugin requires you to prepare.
- Classic tokens require the
gistscope. - Fine-grained tokens have limited support for Gists; the README suggests using a classic token.
- Fill it in the “GitHub Token” input box in the plugin page; the token is saved to DSH’s credential service and not written to the plugin configuration file.
- When filling it in for the first time, click “How to get Token,” which will first show the required permissions and steps, then provide a link to “Open GitHub Token Page,” with the description and
gistpermission pre-filled. - If the environment variable
GITHUB_TOKENalready exists, the page will show “Configured,” and you can leave it empty without modification.
Without a token, you can still read accessible Gists and preview, but you cannot create or push.
Typical Usage: First Host Creating the Gist¶
After configuring the token, the operations for the first host are:
- Keep “Gist Name” as the default
dsh-plugin-sync. - Click “Create Gist and Push Local Manifest.”
The plugin will do the following in sequence:
- Create a secret Gist, not a public one.
- Write to the default filename
dsh-plugins.json. - Write the host-portable profile bundle manifest to
hosts.<hostId>. - Write the Gist name and ID to the local state.
- Display the Gist name, ID, and URL on the page.
Afterwards, just tell the other hosts the Gist name. They will search by name under the same GitHub account.
Typical Usage: Other Hosts Pulling and Conflict Confirmation¶
Other hosts fill in the same name in “Gist Name,” save the configuration, and then click “Pull Online Manifest.” The plugin uses the current token to search for a Gist with the same name under that account; if not found, it automatically creates one on the first push. No need to create a repository or a new Gist.
Clicking “Pull Online Manifest” is read-only and does not install. The page will display the number of online hosts, the number of plugin records, and mark each item as missing, upgrading, downgrading, source changed, enabled, locally unique, as well as the number of conflicts, source hosts, and recommended selection for each item.
A summary of default conflict rules is as follows:
| Situation | Default Handling |
|---|---|
| Missing locally, enabled online | Install online version |
| Local version is lower | Upgrade to online version |
| Local version is higher | Keep local, do not downgrade |
| Same version but different source | Keep local, avoid sudden source change |
| Locally installed but disabled, enabled online | Keep disabled |
| Multiple online hosts have inconsistent version or source | Use the highest version |
| Only local has it | Keep, can be manually selected for deletion |
| Online is disabled | Do not automatically disable local |
Only items selected for “Use Online Version” or “Delete Local Plugin” will be executed when clicking “Apply Sync.” The preview snapshot is kept for 10 minutes; after it expires, a re-pull is required.
Deletion is an irreversible operation; it calls the official ctx.pluginManager.removeBundle() to uninstall the package and remove the selection from the profile, taking effect after a restart. Therefore, after selecting deletion, the Apply button changes to a danger color and requires a second confirmation. Plugins provided by DSH installation and marked as removable: false only show “Keep.”
When applying sync, the Host calls ctx.pluginManager.installBundle() for install, upgrade, and source change items, ctx.pluginManager.setBundleEnabled() for enabled items, and ctx.pluginManager.removeBundle() for locally unique items selected for deletion. Plugins not selected will not be automatically disabled or deleted. At most maxInstallsPerRun items are processed in a single run, and restart-required may appear; just restart as prompted by DSH.
Plugin Update Tab: Ask Package Sources, Not the Manifest¶
The Gist manifest only knows “the version the host pushed last time” and will lag behind the real release. An example from the README: the manifest writes dshmarket@1.70.2, but on npm it is already 1.73.3.
The Plugin Update tab bypasses the manifest and directly asks the package sources:
| Source | Query Method |
|---|---|
| npm package | GET registry.npmjs.org/<name> dist-tags.latest; if missing, take the largest version number in versions |
| GitHub source code | GET api.github.com/repos/.../releases, take the latest stable release |
| npm package + GitHub repository | Get the repository from the repository field of npm’s full metadata, then query GitHub and compare versions with npm |
There are four rules on the GitHub side:
- Prioritize release over tag. Only fall back to tag if the release list is successfully read and is indeed empty, marking it “GitHub tag (no release)” on the interface. If the request itself fails, an explicit error is reported, and it won’t quietly fall back to the tag.
- Take the latest by published time, not the maximum version number. Default to
published_atof the release; fall back to version comparison only if a timestamp cannot be obtained. - Take only stable versions, filtering out platform builds. Draft,
prerelease: true, versions with pre-release suffixes, and platform versions with+build metadata are skipped. - If versions are the same, use npm. Only select GitHub if the GitHub version is strictly higher.
npm-spec packages will automatically check GitHub; if repository does not exist or is not GitHub, it falls back to checking npm only. The npm query uses Accept: application/json because the repository field only appears in full metadata.
These automatic rules are just default suggestions. Each updatable item in the list has a dropdown box, allowing you to specify “Auto,” “npm,” “GitHub,” or “Do not update” item by item. The flow is:
- Click “Check Latest Version,” logs are output line by line in real-time. After checking each plugin, a line is written immediately, and the progress bar advances synchronously.
- Check the plugins to update, and select the channel for each item as needed; batch operations like “Select All” and “Clear Selection” are available.
- Click “Update Selected Items,” which only re-checks and installs the selected plugins; the log header shows
checking N selected bundle(s).
Sources like file:, link:, https://...tgz cannot be queried locally or packaged sources and will be marked as “Unable to query” rather than guessing a version.
Protection Mechanism for Upgrading Installed Plugins¶
The plugin manager in dsh 0.2.1, when the hmr service exists, returns already-installed for installBundle as long as the package is already in the profile’s dependencies. This means there is no path for in-place upgrades; the plugin page and dshmarket both use “uninstall then install” to bypass this.
This plugin uses the same two-step process but adds three layers of protection because if removeBundle() fails with bundle-in-use, the plugin has already been removed from dsh.profile.bundles, appearing as installed but not effective, and cannot be restored even after a restart.
- Probe first, do not delete blindly. Before upgrading, call
setBundleEnabled(name, false)to probe; if the manager returnsrestart-required, or the plugin is still in theenabledstate, it is determined that HMR cannot uninstall it, and the deletion is abandoned without execution. - Any deletion failure immediately restores the selection. After deletion is rejected, call
setBundleEnabled(name, true)to put the plugin back into the profile, leaving no “installed but disabled” state. - Remember which plugins can only be updated manually. Failed plugins are written to
state.manualUpdates, showing a “Requires Manual Update” badge and reason in the preview. During subsequent syncs, they are skipped directly, and deletion is not attempted again.
The full flow is: install as usual, record the current version when encountering already-installed; skip directly in manualUpdates; after probing confirms it can be uninstalled, write to pendingUpgrades, then removeBundle() and installBundle(); if deletion fails, restore selection; if the new version cannot be installed, reinstall with the old spec and prompt that a rollback occurred; clear pendingUpgrades after success or rollback.
Two exceptions: This plugin will not upgrade or delete itself, because deletion would unload the currently running code; plugins provided by DSH installation with removable: false only show “Keep.”
Manual Update Steps¶
Plugins that failed the probe will not be automatically upgraded but will directly provide a copyable terminal command. An example from the README:
1. Quit DSH completely
2. cd "C:\Users\jht\.dsh\profiles\desktop"
3. "C:\Users\jht\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe" ^
"C:\Users\jht\.dsh\dsh-runtimes\dsh-primary-runtime\dependencies\pnpm\bin\pnpm.cjs" ^
add dsh-mnemon@0.5.36
4. Start DSH again
The key point is: the command uses DSH’s built-in node and pnpm, does not rely on the system PATH, and can be run by copying; it uses pnpm add in one step, no need to remove first, because there is no HMR competition after DSH is closed. The profile directory is derived from DSH_HOME + DSH_PROFILE and can also be overridden with DSH_PROFILE_DIR. Items with the manual field will also have these commands printed by the plugin_sync tool’s apply result.
Error Messages¶
Most rejections from the plugin manager only contain a code without a message. The plugin translates these codes into readable sentences to avoid [object Object] appearing on the page.
| code | Page Display |
|---|---|
already-installed |
This package is already in this profile; upgrade requires uninstalling first |
incompatible-version |
The package requires a DSH version different from the current host |
not-bundle |
This package has no bundle patch and cannot be enabled |
management-required |
This bundle is managed by DSH installation and cannot be modified from the profile |
bundle-in-use |
HMR cannot uninstall this plugin while running; its enabled status has been preserved |
Suitable Scenarios and Notes¶
Suitable scenarios: Having two or three DSH hosts where you want the plugin manifest to be basically consistent but do not want to maintain a public repository manifest; or you want to align the plugin versions on various hosts to the real latest.
Points to note:
- The plugin runs with the permissions of the current DSH process; synchronization and installation actions are essentially the same as manual operations in the plugin page. Think twice before installing, it is best to check the plugin source code first.
- The plugin itself has the MIT license; the manifest file
dsh-plugins.jsonstores the plugin name, version, and source, and does not contain the token. - It only does manual synchronization; relying on it to automatically align on boot is unrealistic.
- Synchronization and installation use DSH’s official plugin manager
ctx.pluginManager, executed in the profile directory by DSH’s built-in pnpm, so you may still encounterrestart-required. - The plugin must be installed once on every host; it does not automatically install itself.
Additionally, here is a note on the ecosystem background: DSH’s community directory is an independent site and has no official affiliation with DeepSeek or Hanhua; this plugin is a community project.
Conclusion¶
The value of dsh-plugin-syncup lies in converging the task of “aligning multi-host plugin manifests” into one pull and apply in the plugin page, previewing conflicts before executing, and having rollback and manual fallback for installation failures. It does not pursue high automation, and its standardization is limited, but it covers the manual synchronization scenario quite completely.
Directory: https://www.skillhub.cn/plugins/jijijitian/dsh-plugin-syncup
Source Code: https://github.com/jijijitian/dsh-plugin-syncup