DeepSeek Harness Plugin Hub

Publish and manage complete Harness Profiles. Discover Plugins for your next setup.

Explore

PluginsPresetsDocsNews

Community

Publish a pluginContactReport an issue

Resources

Plugin Hub on GitHubDeepSeek HarnessSystem statusPrivacy notice
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

Independent and unofficial. Not affiliated with, authorized by, or endorsed by DeepSeek.

DeepSeek Harness Plugin Hub
ProfilesPluginsCategoriesNewsDocsSign inManage Profiles
ProfilesPluginsCategoriesNewsDocsSign in
← Plugins

dsh-update-checker

Update Checker

Auto-check DeepSeek Harness updates for both dsh web and the Desktop app, check third-party plugin updates, notify in the GUI, one-click update with backup/rollback and restart watchdog.

The plugin will be installed here. Keep web if you are unsure.

npx -y @deepseek-ai/dsh plugin --profile web add github:Airmetro/dsh-update-checker#d9b20e1de7298f43a8bd50d0e3565685bb17eeb8
READMECompatibilityVersions

Compatibility and provenance

Update Checker is published as dsh-update-checker and currently resolves to version 1.7.3. The Hub verifies its manifest and preserves the exact installation source for reproducible installs.

DSH compatibility
*
Runtime surfaces
web
Release source
github
Registry updated
11/5/2026

Versions

1.7.3stable
10/21/2026
1.7.2stable
10/12/2026
1.7.1stable
10/6/2026
Show 44 more versionsCollapse versions
1.7.0stable
10/5/2026
1.6.4stable
9/28/2026
1.6.3stable
9/25/2026
1.6.2stable
9/24/2026
1.6.1stable
9/21/2026
1.6.0stable
9/21/2026
1.5.0stable
9/17/2026
1.4.23stable
9/16/2026
1.4.22stable
9/9/2026
1.4.21stable
9/4/2026
1.4.20stable
9/3/2026
1.4.19stable
9/1/2026
1.4.18stable
9/1/2026
1.4.16stable
8/25/2026
1.4.15stable
8/22/2026
1.4.14stable
8/21/2026
1.4.13stable
8/21/2026
1.4.12stable
8/21/2026
Latest
1.7.3
DSH
*
HMR
Process restart
Tree shaking
Safe tree shaking not declared
Unpacked size
Unavailable
Files
Unavailable
Surface
web
License
MIT
Source
github
GitHub
★ 19
Weekly downloads
1,313
Last push
10/21/2026
View source ↗Project homepage ↗
README badge

Click the badge to copy Markdown for your README.

Do you maintain this Plugin?Claim benefit · Priority security scan

Verify the GitHub repository declared in package.json to manage this listing. After you claim it, Hub will prioritize a security scan of the current version and publish the result when it passes.

Claim this Plugin →
Report an issue
1.4.11stable
8/21/2026
1.4.10stable
8/21/2026
1.4.9stable
8/20/2026
1.4.8stable
8/19/2026
1.4.7stable
8/19/2026
1.4.6stable
8/19/2026
1.4.5stable
8/19/2026
1.4.4stable
8/19/2026
1.4.3stable
8/18/2026
1.4.1stable
8/18/2026
1.4.0stable
8/17/2026
1.3.2stable
8/15/2026
1.3.1stable
8/15/2026
1.3.0stable
8/15/2026
1.2.3stable
8/15/2026
1.2.2stable
8/15/2026
1.2.1stable
8/15/2026
1.2.0stable
8/15/2026
1.1.2stable
8/15/2026
1.1.1stable
8/15/2026
1.1.0stable
8/15/2026
1.0.4stable
8/15/2026
1.0.3stable
8/15/2026
1.0.2stable
8/15/2026
1.0.1stable
8/15/2026
1.0.0stable
8/15/2026

README

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

  • v1.7.3 — a pnpm isolated profile no longer collapses the plugin list to the checker itself (issue #40):

    • The scan root was the pnpm store, not the profile. With pnpm's default isolated linker, Node resolves this plugin to its real path (…/profiles/web/node_modules/.pnpm/dsh-update-checker@1.7.2_react@18.3.1/node_modules/dsh-update-checker/lib). Walking up to the first node_modules ancestor therefore landed inside the store, and PROFILE_NODE_MODULES / PROFILES_ROOT / $DSH_HOME all followed it — plugins.json listed exactly one plugin, the checker itself, while the profile really had 14. resolveProfileNodeModules() now escapes .pnpm (including pnpm's own .pnpm/node_modules) back to the profile's node_modules, and when the plugin is installed outside a profiles tree (npm global) it prefers the profile that is actually running ($DSH_PROFILE_DIR, profiles/$DSH_PROFILE) instead of the Node install directory. DSH_UC_PROFILE_NODE_MODULES still overrides everything and is now documented above.
    • Symlinked plugin entries were skipped. scanInstalledPlugins() filtered entries with Dirent.isDirectory(), which is false for a symlink or junction, so every pnpm-installed plugin was dropped before its package.json was read. Entries are now followed (lstat + stat), the profile entry stays the plugin directory (that is what an update replaces), and .pnpm / .bin / the checker's own temp dirs are skipped explicitly. A link: dependency — a plugin linked from a source directory, for example a local preset bundle — is flagged as a local link and reported in ignored with a reason instead of turning into a red "failed" row.
    • Visible instead of silent. The runtime block reports profileLinker (read from node_modules/.modules.yaml) and carries a profile-isolated-linker warning; the settings panel renders it, so a profile whose pnpm-workspace.yaml lost nodeLinker: hoisted can no longer silently look like "one plugin installed".
    • Tests: 323 passing (node --test "scripts/*.test.mjs"), with scripts/unit-isolated-linker.test.mjs (store escaping on Windows and POSIX paths, , the explicit override, npm-global → running profile, parsing) and , which builds a real isolated layout — a store with junction entries, and the plugin itself imported — then asserts that the scan root is the profile's , that every entry is listed with its profile path and that the store and real-directory plugins carry their versions, that only the source entry is flagged as a local link, that is , and that the hoisted layout is unchanged. On 1.7.2 the same layout cannot produce that list — entries are filtered by and nothing escapes — which is the in the report. renders the real settings section through the module loader and asserts the layout hint appears for , stays absent for and for a host that sends no block, and that both language dictionaries carry it.

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

DSH_UC_NODE_EXE
mise which node
asdf which node
node_modules_<major>
nodejs24-npm
/usr/lib
/usr/local/lib
/opt/homebrew/lib
/usr/local/opt/npm/lib
$npm_config_prefix
npm
npm.cmd
PATH
  • Restart launcher — self-adapting: DSH_UC_LAUNCHER / DSH_RESTART_LAUNCHER, then common launcher names under the deployment root (DeepSeek Harness.cmd, start-dsh.cmd, …); the chosen launcher is spawned with a visible window, so the restarted server has a console (v1.6.3 — a hidden spawn left an orphan holding the port and swallowed the access URL). The web port is read from the running webServer.port.
  • Tuning env vars — DSH_UC_UPDATE_PORT sets the port the update worker stops/starts/probes (default 3080), and DSH_UC_RESTART_WINDOW_MS sets how long the worker keeps observing a slow first start before giving up (default 150000; the progress record streams the whole time).
  • When POSIX cannot be done safely, /update still answers 501 E_PLATFORM_UNSUPPORTED: the service process can only be identified reliably when ss or lsof is present. Install one of them (iproute2, lsof), or stop DSH and update manually.
  • Plugin updates — behavior is unchanged and they target the running profile (profiles/desktop). On Desktop the bundled pnpm is preferred for lockfile sync (resources/runtime/pnpm/bin/pnpm.mjs, launched as Electron --expose-internals … with ELECTRON_RUN_AS_NODE=1 and the bundled runtime/bin on PATH), and when the machine ships no npm at all the staging install falls back to that bundled pnpm (pnpm add <spec> --dir <tmp> / pnpm install --dir <root> --prod, build scripts via pnpm rebuild) instead of failing. DSH_UC_NODE_EXE still overrides the Node used for npm.
  • Settings panel — on Desktop it shows the runtime source, channel and feed, hides the npm-only controls (pre-release switch, download source) and explains that the core updates itself.
  • .pnpm/node_modules
    .modules.yaml
    scripts/integration-isolated-scan.test.mjs
    .pnpm
    from inside the store
    node_modules
    link:
    runtime.profileLinker
    isolated
    Dirent.isDirectory()
    .pnpm
    plugins: ["dsh-update-checker"]
    scripts/integration-client-settings-linker.test.mjs
    isolated
    hoisted
    runtime
  • v1.7.2 — the Desktop update feed is named per platform, so the main-program check works on macOS again (issue #38):

    • <channel>.yml was only correct on Windows. desktopFeedUrl() built the electron-builder manifest name from the channel alone, but electron-builder writes <channel>-mac.yml on macOS and <channel>-linux.yml on Linux — the packaged app-update.yml points at a per-platform directory, not at a per-platform file name. On macOS the plugin therefore requested …/feeds/mac-arm64/nightly.yml (404) while the file that exists is …/feeds/mac-arm64/nightly-mac.yml (200), so every main-program check on macOS ended in status: "error" / desktop update feed HTTP 404 and the settings card showed latest: null. Measured against the live feed: …/mac-arm64/nightly.yml → 404, …/mac-arm64/nightly-mac.yml → 200, …/win-x64/nightly.yml → 200 (Windows keeps the bare name), …/win-x64/nightly-win.yml → 404.
    • The file name is now derived from the platform. desktopFeedName(channel, platform) (darwin → -mac, linux → -linux, anything else bare) feeds desktopFeedUrl(baseUrl, channel, platform), with the platform as an injectable parameter like the other platform helpers in the file, so every branch is testable on any host. A 404 on the platform name is treated as a name mismatch and falls back once to the bare <channel>.yml (any other status, and network errors, are reported as they are); the fallback is recorded in the ops log as main-update-desktop-feed-fallback, and the URL that actually answered is what status.json and the settings card report.
    • Tests: 306 passing (node --test "scripts/*.test.mjs"), with the new scripts/unit-desktop-feed-url.test.mjs (per-platform names and candidate order, a macOS host reading <channel>-mac.yml alone, the 404 fallback plus its ops-log entry, the Windows single-request path, both names missing reported against the primary name with attempts/lastError, a 500 that must not fall back, a versionless manifest, and the DSH_UC_FEED_URL override), and scripts/integration-desktop-host.test.mjs now asserts that the first feed request behind a real status.json is the platform-correct manifest. The live macOS feed was read through the real code path: version = 0.2.0-rc.2, feedUrl = …/feeds/mac-arm64/nightly-mac.yml, download …/bin/mac-arm64/deepseek-harness-0.2.0-rc.2-mac-arm64.zip; the same build against the Windows feed still requests …/feeds/win-x64/nightly.yml.
  • v1.7.1 — the checker now knows which host it runs on, and adapts to the Desktop (Electron) application while keeping the web behavior:

    • Runtime platform detection. detectDesktopRuntime() classifies the host from the Electron runtime, the dsh-desktop-host entry in process.argv, process.resourcesPath and a packaged app-update.yml, and every status response now carries platform plus a runtime block (managedBy, Electron version, app root, resources/runtime dirs, update source availability, channel, feed); DSH_UC_FORCE_PLATFORM overrides it for tests.

    • The core version on Desktop comes from the application's own update feed. Installed = the packaged app version (app.asar/package.json, fallback app.asar/dsh/package.json); latest = <url>/<channel>.yml resolved from app-update.yml (generic provider, nightly), parsed with a dependency-free reader that handles the folded-scalar url: >- blocks the feed uses. On web nothing changed: npm/GitHub remain the source, and the previously broken installed: null on Desktop is gone.

    • Desktop updates are delegated, never performed. POST /update on Desktop answers 409 E_DESKTOP_OWNED with action: "open-native-update", the target version and channel, and writes neither the update lock nor a progress record — the plugin cannot and must not npm-install into the signed app.asar. /rollback answers 409 for the same reason (no plugin-owned backup of a bundled runtime).

    • Desktop restart is manual, by design. /restart answers 409 E_DESKTOP_RESTART_MANUAL with a guide instead of killing the Host that Electron supervises (tray menu → “Quit DeepSeek Harness” → reopen); the settings panel shows the same guide where the web build shows “Restart service”.

    • Desktop package manager. Lockfile sync prefers the bundled pnpm (argv[5] of the Desktop host, run through Electron --expose-internals with ELECTRON_RUN_AS_NODE=1, DSH_DESKTOP_NODE_EXECUTABLE and the bundled runtime/bin on PATH), falling back to a discovered pnpm if that invocation fails; npm keeps its existing resolution, and on Desktop the Electron executable is used as the Node for it when no real Node exists. Plugin staging installs still use npm when it is available and fall back to the bundled pnpm (pnpm add --dir / pnpm install --dir --prod, pnpm rebuild for build scripts) when it is not.

    • Client awareness. The overlay detects the dsh-app:// origin and window.dshDesktop.updates, renders Open update window as the primary action on Desktop (never the npm install button), subscribes to Electron's update state to show download/verify/install progress in the banner, and keeps the npm confirm/update flow untouched on web. The settings section reports the Desktop runtime, channel and feed, hides the pre-release switch and download-source selector, and plugin-update results say “takes effect after restarting the desktop app”.

    • A mount can no longer downgrade a real installation. ensurePluginMount() converts the plugin copy found in each profile into a link to the install it is running from. It did that for real directories unconditionally, so a profile holding a newer real copy (1.7.0) was silently replaced by a link to an older shared copy (1.4.23). linkPackageIntoProfile() now compares versions before replacing a real directory and refuses when the destination is newer (conflict: "downgrade", reported in the mount errors, nothing written to disk); equal or newer sources are deduplicated exactly as before.

    • Tests: 293 passing (node --test "scripts/*.test.mjs"), with scripts/unit-desktop-runtime.test.mjs (detection across web/Electron/argv/override cases, app-update.yml and feed parsing of the real payload shapes, feed-URL joining, pnpm staging args), scripts/integration-desktop-host.test.mjs (real route handlers against a local HTTP feed: Desktop status/versions, E_DESKTOP_OWNED with no lock/progress file, the restart guide, rollback refusal, plugins.json platform), scripts/integration-client-desktop.test.mjs (the actual overlay component rendered through the module loader with a fake React: Desktop renders the native action, a downloading state renders 42 % progress, web still posts to /update) and scripts/integration-mount-guard.test.mjs (older source vs newer real copy is refused, newer source still deduplicates, a pre-release destination counts as newer). scripts/test-client-apply.mjs carried a stale worker-milestone assertion that already failed on untouched 1.7.0; it now checks that the worker's milestone percentages are non-decreasing.

    • Verified on a real installation — the build was driven inside the genuine Electron-as-node process (DeepSeek Harness.exe 44.0.0, the installed app's resources) against a mirror profile: 24/24 checks passed, including installed = 0.2.0-rc.2 read from the signed app.asar, latest = 0.2.0-rc.2 fetched live from https://download.deepseek.com/dsh-desk/feeds/win-x64/nightly.yml, channel = nightly, /update → 409 E_DESKTOP_OWNED with no update lock, progress record or backup directory written, /restart → 409 E_DESKTOP_RESTART_MANUAL with the guide, and /rollback → 409. The same build in a plain Node process reported platform: web, source: npm and still took its normal npm/deploy-tree path (9/9 checks).