dsh-update-checker
English | 中文
A permanent Cordis plugin for the DeepSeek Harness Web GUI that auto-checks for new DeepSeek Harness releases and installed third-party plugin updates (the former standalone dsh-plugin-checker was merged in v1.1.0), asks the user, and one-click updates with success/failure feedback.
Features
- Full update lifecycle — check, backup, update, rollback, and restart, all in one plugin.
- Main program check — compares the installed
@deepseek-ai/dsh against the npm latest (full packument, stable-first, semver-aware). A pre-release target is installed only when it belongs to the channel the deployment already follows — a cross-channel promotion (rc → alpha) is refused with E_PRERELEASE unless you enable the allowPrerelease setting, so the checker never promotes the harness into an unintended pre-release channel.
- Third-party plugin check — scans installed non-official plugins (layout-agnostic, incl. pnpm-hoisted
node_modules), cross-compares each against npm + GitHub (target = higher version); local tools with no publish source go to ignored. When a plugin name has multiple copies, the one in the composition-owning profile's node_modules wins (the rest are listed as copies), and each plugin can be excluded from prompts (excludedPlugins, re-enableable in the settings page).
- Working GitHub channel — dedicated HTTPS client for GitHub domains (tolerates self-signed local proxies; the npm registry still uses strict TLS), with redirects, size caps and timeouts; codeload tarballs are validated before install.
- In-GUI banner — locale-aware (zh/en follows the DSH UI language), states update / up-to-date / failure, with a suppression flag and a change brief (vX→vY + risk level + release notes when available).
- One-click update with safety — main program: dry-run guard (aborts when the plan would remove a package the deployment itself declares; npm reclaiming the update target's own transitive dependencies is recognised and allowed, and
allowRemove overrides it explicitly) → snapshot backup (version manifests + a main-snapshot copy of the @deepseek-ai tree for offline rollback) → layout-adaptive install (in-place or -g) → post-install check installed==latest; plugins: temp-dir install + copy, dependency version reconciliation, auto --allow-scripts for native deps on npm ≥ 12, a downgrade guard (a rate-limited GitHub probe can no longer make the installer fall back to an older npm release) and a rename-free in-place sync for plugins whose directory the running host keeps pinned. Updates (and rollbacks) persist to the profile package.json + lockfile (pnpm install --lockfile-only / npm install --package-lock-only), so a later install never silently reverts the plugin — no more "same plugin keeps asking for the same update" loops.
- Real rollback — main program via
POST /rollback, plugins via POST /plugin-rollback; GET /backups.json lists both.
- Restart with watchdog — launcher derived from the current process argv, kill by PID + port, recovery confirmed by port listening, an HTTP 200 probe (
GET /restart-status.json) and a new instance id read back from this plugin's own routes, so "something answers on the port" is no longer mistaken for "the updated build came up".
- Write-route security — all write routes require
{ "confirm": true } and a loopback source (127.0.0.1/::1), so LAN clients can't trigger update/restart/rollback.
- Zero-config portability — profile dir / composition file / deploy root are derived from the plugin's own install location, while state, backups and logs honour
DSH_HOME (then ~/.dsh); works on any machine without editing code.
- Self-mounting on
dsh 0.1.6-alpha.2+ — that release switched the profile resolution default from "link" to "runtime", which stops a third-party plugin in $DSH_HOME/profiles/node_modules from resolving and kills the launch with ERR_MODULE_NOT_FOUND before the server binds. The plugin now re-establishes its own mount at startup (ensurePluginMount): a junction from every profile's node_modules to the real package, plus the dependencies declaration each profile needs so ownership is recognised. Idempotent, never overwrites a spec you set deliberately, never deletes a directory it cannot prove is its own copy; mount.json / status.json report the state.
Host & Client
- Host (
lib/index.js) — HTTP routes: status.json (check), mount.json (self-mount state), suppress, update (with dry preview), rollback, backups.json, restart, restart-status.json, plugins.json, plugin-update, plugin-rollback, plugin-exclude.
- Client (
lib/client.js) — renders two banners in the root shell.overlay slot: a core banner (main-program update state) and a plugin banner (updatable plugins with single / update-all buttons). Both check on page load, then every 6 hours; the settings page ("检查更新") adds rollback buttons.
Install & mount
The package is a profile bundle (its manifest declares dsh.bundle.patch).
# 1) put the package into $DSH_HOME/profiles/node_modules/ so the profile can resolve it.
# ⚠️ Never run `npm install` directly inside $DSH_HOME/profiles — it has no
# package.json and npm would prune the whole node_modules (data loss).
# Safe option A — install in a temp dir, then copy only this package:
npm i dsh-update-checker --prefix <temp-dir> --no-save
cp -r <temp-dir>/node_modules/dsh-update-checker $DSH_HOME/profiles/node_modules/
# Safe option B — copy the package directory manually (git clone or tarball).
# 2) add the row to $DSH_HOME/profiles/web/cordis.patch.yml
# $DSH_HOME/profiles/web/cordis.patch.yml
- insert:
- id: dsh-update-checker
name: 'dsh-update-checker'
dsh 0.1.6-alpha.2 and later: the profile needs its own link
Step 1 alone is no longer enough. 0.1.6-alpha.2 changed the profile module-resolution
default from "link" to "runtime", which turns $DSH_HOME/profiles/node_modules into a
shared managed directory that dsh excludes from Node's native resolve. It now only serves the
deployment dependency closure and the selected bundle closure (see PluginPackages /
routeScoped in @deepseek-ai/dsh-app-boot), and a third-party plugin belongs to neither — so
the bare dsh-update-checker name stops resolving and the launch dies with
ERR_MODULE_NOT_FOUND: Cannot find package 'dsh-update-checker' imported from …\profiles\web\
before the web server binds. (That importer path is rewritten by dsh to point at the profile
directory; the real failing base is $DSH_HOME/package.json. Do not trust the path in the
message.) Two things fix it, and both are needed:
# 2a) link the profile's node_modules at the real package (junction, never a copy)
New-Item -ItemType Junction `
-Path "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-update-checker" `
-Target "$env:USERPROFILE\.dsh\profiles\node_modules\dsh-update-checker"
# 2b) declare the dependency in the profile manifest
# $DSH_HOME/profiles/web/package.json → "dependencies": { "dsh-update-checker": "^1.6.1" }
A junction is required rather than a copy: the link's real path must stay
…/profiles/node_modules/…, or pickDshHome can no longer recognise the Harness home and the
plugin's self-location drifts (which would send its @deepseek-ai/* sync to the wrong place).
The declaration is what dsh's readProfilePlugins and this plugin's own
findDeclaringProfiles/persistPluginUpdate read to decide which profile owns the plugin —
without it the plugin reports itself as permanently outdated.
Step 2 is not optional on a fresh install. A profile that cannot resolve the plugin dies in
composeProfile — before the plugin is loaded — so no code inside the plugin can repair that
first launch. Do 2a and 2b by hand when you install (or when you add a second profile), and the
profile comes up.
After that first successful launch the plugin maintains the mount itself. From v1.6.0 it
re-runs ensurePluginMount at startup, after each plugin update and after each plugin rollback:
it creates or repairs the link in every profile and writes the declaration into every harness
profile, is idempotent, never overwrites a link:/file: spec you set deliberately, and never
deletes a directory it cannot prove is its own copy. So a later dsh upgrade, a new profile
(same machine), a dropped link or a changed version are all repaired without hand-editing again.
Read the state any time at GET /dsh-update-checker/mount.json (read-only); it is also carried in
status.json as mount. POST /dsh-update-checker/mount forces a re-check (needs
{ "confirm": true } + loopback, like every other write route).
Then let patch HMR apply it (or restart dsh web) and reload the page.
Step-by-step guide with troubleshooting (中文): docs/INSTALL.md.
Configuration & portability
All paths are auto-detected at runtime — nothing is hardcoded:
- Plugin / profile dir — derived from the plugin's own install location (
import.meta.url), with two escape hatches: in a pnpm isolated profile the real path is inside node_modules/.pnpm/…, so the chain is resolved back to the profile's own node_modules; and when the plugin is installed outside a profiles tree (npm global) the profile that is actually running ($DSH_PROFILE_DIR, profiles/$DSH_PROFILE) is preferred. DSH_UC_PROFILE_NODE_MODULES overrides the result completely (v1.7.3, issue #40).
$DSH_HOME — the parent of the profiles root (state, backups, restart log live there).
- Composition file — the profile that is actually running wins:
$DSH_PROFILE_DIR/cordis.patch.yml → profiles/$DSH_PROFILE/… → the profile whose patch names this plugin → $DSH_HOME/profiles/web/cordis.patch.yml. (v1.6.3; before that the web default was used whenever nothing proved otherwise, so a host serving another profile read the wrong node_modules — issue #31.)
- Deployment root — junction
realpath first, then DSH_DEPLOY_ROOT, then process.cwd(), then the npm global prefix (parent of npm root -g's output; v1.4.9+ covers npm -g installs).
- systemd /
npm -g escape hatch: if auto-detection ever misses your setup, set DSH_DEPLOY_ROOT to the directory that contains node_modules/@deepseek-ai/dsh (e.g. <npm prefix>/lib on Linux).
- Node / npm executables —
resolveNodeExe() finds the real Node: DSH_UC_NODE_EXE override → npm_node_execpath → process.execPath when it is Node → common install dirs → PATH. This is what makes DSH Desktop (Electron, where process.execPath is electron.exe) able to run npm for plugin updates. If your Desktop build bundles Node elsewhere, set DSH_UC_NODE_EXE to it. If your Node is managed by mise / asdf / nvm and your PATH only exposes the version-manager shim (e.g. ~/.local/share/mise/shims/node), the shim directory has no npm beside it; v1.4.22+ resolves the real binary by running node -p process.execPath through the shim. If that still fails (or you want to skip the lookup), set to the real binary, e.g. / . v1.6.3 searches npm more widely: beside Node, in (Fedora/RHEL ), , , , , , and through every / on — including the real target behind a shim. A packaged desktop app that ships no npm at all uses its own bundled pnpm for plugin updates instead (v1.7.1+); replacing the package-manager call with a built-in installer is still open (issue #32).
Platform & install-layout support
- Detection (checks) — layout-agnostic, works on any machine.
- One-click update & restart — no longer Windows-only:
- Service stop/start probes use
Get-NetTCPConnection + taskkill on Windows and ss -H -tlnp "sport = :<port>" (fallback lsof -tiTCP:<port> -sTCP:LISTEN) + SIGKILL on Linux/macOS. The port is always named explicitly, so an unrelated listener can never be matched; on POSIX a PID is only killed when /proc/<pid>/cmdline is unreadable or names node/dsh.
- The POSIX restart watchdog is
scripts/restart-watchdog.sh, the counterpart of scripts/restart-watchdog.ps1. It takes the same environment variables — DSH_RESTART_PORT, DSH_RESTART_PID, DSH_RESTART_NODE_FILE, DSH_RESTART_NODE_ARGS (JSON array), DSH_RESTART_LAUNCHER, DSH_RESTART_WORKDIR, DSH_RESTART_LOG, DSH_RESTART_RESULT — and writes the same result JSON (startedAt, port, pid, recovered, recoveredAt, attempts, error). It relaunches via the node argv first, then systemctl --user restart dsh-web.service, then the launcher path passed as a single argument (a path containing spaces survives); if none of those exist it reports no launcher available instead of pretending to recover. It is invoked as sh <script>, so no executable bit is required.
- Main-program update adapts: in-place
npm install when the deploy root has a package.json, npm install -g otherwise; both run the dry-run guard and re-read the installed version afterwards.
- Plugin updates — temp-dir install + copy, npm 11/12+ compatible.
- Profile layouts — both npm-style
hoisted profiles and pnpm's default isolated layout are supported: plugin entries that are symlinks or junctions are followed when listing (and the profile entry, not the store path, is what an update replaces), .pnpm itself is never treated as a plugin, and the runtime block reports profileLinker (isolated / hoisted / null) together with a visible warning in the settings panel when the profile uses the isolated layout (v1.7.3, issue #40).
Desktop (Electron) support
The plugin decides at runtime whether it is hosted by dsh web (plain Node) or by the Desktop application (Electron, ELECTRON_RUN_AS_NODE=1), and reports the result as platform plus a runtime block in /dsh-update-checker/status.json and /dsh-update-checker/plugins.json:
- Detection — Electron runtime (
process.versions.electron), the Desktop host entry (…/dsh-desktop-host/lib/index.js in process.argv), process.resourcesPath and a packaged app-update.yml. DSH_UC_FORCE_PLATFORM=desktop|web overrides it; DSH_UC_RESOURCES_DIR, DSH_UC_RUNTIME_DIR, DSH_UC_APP_VERSION and DSH_UC_FEED_URL exist for tests and unusual layouts.
- Version check — Desktop carries its runtime inside the signed bundle (
resources/app.asar), so installed comes from app.asar/package.json (fallback app.asar/dsh/package.json) and latest comes from the very feed Electron uses: app-update.yml (provider / url / channel, nightly here) → <url>/<channel><platform suffix>.yml — <channel>.yml on Windows, <channel>-mac.yml on macOS, <channel>-linux.yml on Linux (v1.7.2, issue #38), with a one-shot fallback to the bare name when the platform name answers 404 (main-update-desktop-feed-fallback in the ops log). version, releaseDate, installer URL and sha512 are read from that manifest. The npm registry is not consulted for the core on Desktop.
- One-click update — Desktop installs its own release (download → task check → install → restart), so the banner offers Open update window, which calls
window.dshDesktop.updates.open() (the preload bridge exposed to dsh-app://app documents) and mirrors Electron's own update state (downloading / verifying / ready / installing) through updates.subscribe(). POST /dsh-update-checker/update answers 409 E_DESKTOP_OWNED with action: "open-native-update" and writes no progress or lock record: the plugin never npm-installs into app.asar. The web path (npm / tarball, backup, rollback, restart watchdog) is unchanged.
- Rollback —
409 E_DESKTOP_OWNED: the bundled runtime has no plugin-owned backup to restore; reinstall the matching Desktop release from the official feed instead.
- Restart —
409 E_DESKTOP_RESTART_MANUAL plus a localized guide. Electron owns the profile and the Host process, so the plugin does not kill it; after a plugin update, quit from the tray menu (“Quit DeepSeek Harness”) and reopen the application.
Notes
- Host code changes require a service restart (the loader caches imported modules); client changes are picked up by HMR and apply on the next page refresh.
- Update/rollback/restart/suppress/settings routes are guarded by
{ "confirm": true } and a loopback-source check (127.0.0.1/::1).
- Before
npm install, a backup (deployment package.json + package-lock.json + both @deepseek-ai version manifests + backup-meta.json + a main-snapshot copy of the @deepseek-ai framework tree) is written to $DSH_HOME/dsh-update-checker-backups/<timestamp>/; both main-program and plugin rollback routes are provided, and main-program rollback restores from the main-snapshot when present instead of re-installing from the registry.
Changelog
Development
lib/index.js — Host half: plain ESM, Node built-ins only, no build step; pure helpers exported as named ESM exports for unit testing.
lib/client.js — Client half: plain JS (window.__ModuleLoader__), requires only react, no build step.
- Tests:
npm test (Node ≥ 20 built-in test runner, no third-party deps).
scripts/restart-service.ps1 — manual restart helper (run with -ExecutionPolicy Bypass).
License
MIT