Update management for DeepSeek Harness and its plugins: semver-aware dual-source (npm/GitHub) checks for the main program and every installed plugin, a locale-aware banner (zh/en) listing updatable items, one-click updates (the main program is backed up, integrity-checked and rolled back on failure; plugins install via a temp directory without touching other packages), and a watchdog-guarded restart after updating. The settings page adds per-plugin version lamps, live update progress, and banner/notification toggles.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-update-checker
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:Airmetro/dsh-update-checker
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
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/dshagainst 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 withE_PRERELEASEunless you enable theallowPrereleasesetting, 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 toignored. When a plugin name has multiple copies, the one in the composition-owning profile'snode_moduleswins (the rest are listed ascopies), 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 (abort if the plan contains
remove) → snapshot backup (version manifests + amain-snapshotcopy of the@deepseek-aitree for offline rollback) → layout-adaptive install (in-place or-g) → post-install checkinstalled==latest; plugins: temp-dir install + copy, dependency version reconciliation, auto--allow-scriptsfor native deps on npm ≥ 12. Updates (and rollbacks) persist to the profilepackage.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 viaPOST /plugin-rollback;GET /backups.jsonlists 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
dsh0.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_modulesfrom resolving and kills the launch withERR_MODULE_NOT_FOUNDbefore the server binds. The plugin now re-establishes its own mount at startup (ensurePluginMount): a junction from every profile'snode_modulesto the real package, plus thedependenciesdeclaration 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.jsonreport the state.
Host & Client
- Host (
lib/index.js) — HTTP routes:status.json(check),mount.json(self-mount state),suppress,update(withdrypreview),rollback,backups.json,restart,restart-status.json,plugins.json,plugin-update,plugin-rollback,plugin-exclude. - Client (
lib/client.js) — renders two banners in the rootshell.overlayslot: 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). $DSH_HOME— the parent of theprofilesroot (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 thewebdefault was used whenever nothing proved otherwise, so a host serving another profile read the wrongnode_modules— issue #31.) - Deployment root — junction
realpathfirst, thenDSH_DEPLOY_ROOT, thenprocess.cwd(), then the npm global prefix (parent ofnpm root -g's output; v1.4.9+ coversnpm -ginstalls).- systemd /
npm -gescape hatch: if auto-detection ever misses your setup, setDSH_DEPLOY_ROOTto the directory that containsnode_modules/@deepseek-ai/dsh(e.g.<npm prefix>/libon Linux).
- systemd /
- Node / npm executables —
resolveNodeExe()finds the real Node:DSH_UC_NODE_EXEoverride →npm_node_execpath→process.execPathwhen it is Node → common install dirs →PATH. This is what makes DSH Desktop (Electron, whereprocess.execPathiselectron.exe) able to run npm for plugin updates. If your Desktop build bundles Node elsewhere, setDSH_UC_NODE_EXEto it. If your Node is managed by mise / asdf / nvm and yourPATHonly exposes the version-manager shim (e.g.~/.local/share/mise/shims/node), the shim directory has nonpmbeside it; v1.4.22+ resolves the real binary by runningnode -p process.execPaththrough the shim. If that still fails (or you want to skip the lookup), setDSH_UC_NODE_EXEto the real binary, e.g.mise which node/asdf which node. v1.6.3 searches npm more widely: beside Node, innode_modules_<major>(Fedora/RHELnodejs24-npm),/usr/lib,/usr/local/lib,/opt/homebrew/lib,/usr/local/opt/npm/lib,$npm_config_prefix, and through everynpm/npm.cmdonPATH— including the real target behind a shim. A packaged desktop app that ships no npm at all still needs one installed (issue #32). - 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 runningwebServer.port. - Tuning env vars —
DSH_UC_UPDATE_PORTsets the port the update worker stops/starts/probes (default3080), andDSH_UC_RESTART_WINDOW_MSsets how long the worker keeps observing a slow first start before giving up (default150000; the progress record streams the whole time).
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+taskkillon Windows andss -H -tlnp "sport = :<port>"(fallbacklsof -tiTCP:<port> -sTCP:LISTEN) +SIGKILLon 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>/cmdlineis unreadable or names node/dsh. - The POSIX restart watchdog is
scripts/restart-watchdog.sh, the counterpart ofscripts/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, thensystemctl --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 reportsno launcher availableinstead of pretending to recover. It is invoked assh <script>, so no executable bit is required. - Main-program update adapts: in-place
npm installwhen the deploy root has apackage.json,npm install -gotherwise; both run the dry-run guard and re-read the installed version afterwards. - Plugin updates — temp-dir install + copy, npm 11/12+ compatible.
- Service stop/start probes use
- When POSIX cannot be done safely,
/updatestill answers501 E_PLATFORM_UNSUPPORTED: the service process can only be identified reliably whenssorlsofis present. Install one of them (iproute2,lsof), or stop DSH and update manually.
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 (deploymentpackage.json+package-lock.json+ both @deepseek-ai version manifests +backup-meta.json+ amain-snapshotcopy of the@deepseek-aiframework 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 themain-snapshotwhen present instead of re-installing from the registry.
Changelog
v1.6.4 — the tarball fallback installs what a release adds, and the integrity check proves it (issue #33):
- A weak network no longer silently drops the new dependencies (#33). When the npm dry-run gate times out, a core update falls back to
installVia=tarball, whose work list came fromcollectUpdateTodo()— areaddir()of the local@deepseek-aitree. A package the new release adds has no local directory, so it could never be listed, and third-party scopes were never enumerated at all: the 0.1.7-rc.1 → 0.1.7-rc.2 update applied 267 of the 585 packages in the lockfile, leftdsh-client-shortcuts,dsh-client-ui-shortcuts,dsh-experimental-auto-review,dsh-llm-deepseek-account,dsh-llm-deepseek-api-key,dsh-util-code-language,@js-temporal/polyfillandjsbiuninstalled, and still reportedmain-update-ok— port 3080 answered while the plugin/frontend imports failed. The worker now walks the target version's dependency closure from the registry (resolveTargetClosure(), reusing the existingsatisfies()/compareVersions()helpers) and plans only what the deployment cannot already resolve: missing packages, plus@deepseek-ai/*packages whose resolved version differs. Anything already satisfying its range is left exactly as it is, so npm's nested duplicates (debug@2under a^4dependent, …) are never flattened; platform-incompatible optional dependencies (73 on the reporting machine) and third-party packages that would need an install script are skipped and reported. Extracted packages land innode_modules/<name>(a new@scope/directory is created when needed), and the plan, skips and failures are written to the ops log (main-tarball-plan-ok/-incomplete,main-tarball-metadata-failed,main-tarball-plan-conflict). - Completeness is now part of the integrity check (#33).
verifyTree()only walked the directories that existed, so an uninstalled package could not fail it — that is why the partial install above rolled forward as a success. It now checks the resolved closure as well: a missing or wrong-version package is an integrity problem and the update rolls back, turning a silent half-broken deployment into an honest failure. When the registry cannot be reached the closure is reportedincompleteand the previous local-only behavior is kept, so the fallback is never worse than before. - Downloads keep their timeouts.
httpGetBuffer()aborts on a 20 s idle gap or the per-attempt cap, retries three times with escalating 60/90/180 s caps, and never reuses a socket — the long keep-alive connection to the registry was observed degrading until a single package needed 10–20 minutes. - Tests: 244 passing (
node --test "scripts/*.test.mjs"), includingscripts/integration-tarball-closure.test.mjs(a mock registry covering added, transitive and@scopepackages, strict-context upgrades, platform and build-script skips, registry-unreachable fallback, and an integrity case that must fail when a package is removed) andscripts/unit-tarball-timeout.test.mjs(idle/attempt timeouts, size cap, HTTP status, no keep-alive reuse, bounded download pool). Verified against the real registry on a tree with exactly those 8 packages removed: the plan selected exactly those 8, all 12 downloads extracted with the expected versions, andverifyTree()reported 0 problems.
- A weak network no longer silently drops the new dependencies (#33). When the npm dry-run gate times out, a core update falls back to
v1.6.3 — a post-update restart you can see, package-count progress, and npm discovered off the beaten path (issues #30 #31 #32):
- No more console-less orphan after a core update.
startService()relaunched the server withdetached: true+stdio: "ignore"+windowsHide: true— an invisible instance with no console. It outlives the update worker, keeps holding the web port, and the access URL/token printed at startup is discarded with its output; the next launch then dies withlisten EADDRINUSE 127.0.0.1:3080and a wall of "N required plugins did not activate" (webserveris required, so the whole plugin graph fails to compose). The restart now prefers the deployment's own launcher (DSH_UC_LAUNCHER/DSH_RESTART_LAUNCHER/DeepSeek Harness.cmd/start-dsh.cmd/ …) and spawns it with a visible window; with no launcher it falls back tonode … bin.js web, also visible, and the choice is recorded asmain-update-service-restartin the ops log. - Progress is now "packages downloaded / total packages". The bar no longer rides the dependency-tree creep or counts npm HTTP lines (which include metadata, so it ran ahead of reality). While packages are downloaded/installed the percent is
round(done / total * 100)— 100 of 200 packages is 50%, 198 of 200 is 99% — with已下载 137/273 个包(50%)as the detail. The total comes from the npm dry-run ("added N packages") or the lockfile, and the tarball fallback counts downloaded tarballs the same way. The bar never rewinds, and 100% stays reserved for the finished state. - npm is found on distro layouts and shimmed installs (#30 #32).
npmCliCandidates()also looks innode_modules_<major>(Fedora/RHELnodejs24-npm),/usr/lib,/usr/local/lib,/opt/homebrew/lib,/usr/local/opt/npm/liband$npm_config_prefix;locateNpmCli()resolves everynpm/npm.cmdonPATH(and the real target behind a shim) before failing, and itsENPMCLImessage now names the layouts it searched and theDSH_UC_NODE_EXEescape hatch. A packaged desktop app that ships no npm at all still cannot run plugin updates — that case needs Node/npm installed or an in-app tarball installer (tracked in #32). - The plugin list is read from the profile that is actually running (#31).
findCompositionFile()defaulted toprofiles/web/cordis.patch.ymlwhenever it could not prove otherwise, so on a host serving another profile (the desktop app'sdesktop) the panel read the other profile'snode_modules— reportingdshmarket 1.60.0 → 1.65.0forever while the running profile already had 1.65.0. The composition and itsnode_modulesnow resolve fromDSH_PROFILE_DIR/DSH_PROFILEfirst, still validated against this installation's profiles root. - Tests: 230 passing (
node --test "scripts/*.test.mjs"), with new coverage for the package-count mapping, npm fetch parsing, launcher choice and visibility, running-profile resolution and the Fedoranode_modules_<major>layout; the progress E2E timeline asserts the new contract.
- No more console-less orphan after a core update.
v1.6.2 — one-click core updates on Linux/macOS, and plugin updates that survive a pnpm reify (issues #27 #28 #29, PR #26):
- POSIX core updates (#29, supersedes PR #19): the update worker, the service stop/start probes and the restart route no longer assume Windows PowerShell. The worker is spawned directly (
node <script>, detached) with anerrorhandler that releases the update lock and writes a realerrorprogress record — a failed spawn used to be silent and left the banner stuck at 8%. Port discovery on POSIX usesss -H -tlnp "sport = :<port>", falls back tolsof -tiTCP:<port> -sTCP:LISTEN, and never scans every listener; a PID is killed only when/proc/<pid>/cmdlineis unreadable or names node/dsh, andSIGKILLreplacestaskkill.scripts/restart-watchdog.shmirrorsrestart-watchdog.ps1(same environment variables, same result JSON) and relaunches through the node argv first, thensystemctl --user restart dsh-web.service, then the launcher path as one argument. Windows is untouched (Get-NetTCPConnection,taskkill /T /F, the PowerShellStart-Processwrapper). The501 E_PLATFORM_UNSUPPORTEDfail-fast is kept for hosts that have neitherssnorlsof. - Peer-only plugins install again (#28):
buildStageInstallArgs/buildPluginInstallArgsnow pass--legacy-peer-deps. The staging prefix is a throwaway tree and the plugin's peers are supplied by the DSH host at runtime, so npm must not resolve them against the registry — there a*peer lands on a package that does not exist (@deepseek-ai/dsh-compact,E404) and every peer-only plugin died withERESOLVE. PROFILES_ROOTunderstands per-profilenode_modules(#27, PR #26):dirname(profileNodeModules)names the profiles directory only in the shared layout. For…/profiles/<name>/node_modulesit resolved to the profile itself, sofindDeclaringProfiles()found no manifest, the version was never written back topackage.json, and the lockfile was never advanced (persistedManifest:0, andpersistedLock:truewas[].every(...)). The nextpnpm add/pnpm installthen reified the tree from that stale lockfile and rolled every previously updated plugin back to the frozen version.pickProfilesRoot()handles both layouts, andpickDshHome()follows it so a customDSH_HOMEis derived instead of falling back to~/.dsh.
- POSIX core updates (#29, supersedes PR #19): the update worker, the service stop/start probes and the restart route no longer assume Windows PowerShell. The worker is spawned directly (
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 onlyreact, 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
Links
More in this category
yjh051108/dsh-routing-suite★ 7003
One repository, three parts: a runtime injector for DSH plugin packages (inject, hot-reload, unload, promote a dev staging tool to the front, route self-heal, plus a settings-page plugin manager that lists, unloads and drags folders in to internalize), a task-aware reasoning-mode router agent preset (router-standard / router-spec / router-react), and a graded two-level task protocol whose six tools (commit_star, lock_stage, revise_do, edit_plan, mark_task, redteam_verdict) pin task state to disk. The injector implementation ships in-tree, so the install carries its own behaviour rather than a dependency list.
strukto-ai/mirage#dsh★ 3666
Swaps the filesystem and bash providers for a mirage virtual workspace: file tools and shell commands run over mounted resources (RAM, S3, Redis, Slack, Gmail, Notion, Postgres) instead of the host disk, with per-mount read/write/exec modes, per-command sandbox routing (monty, pyodide, quickjs in process; docker, e2b, daytona remote), and installed CLIs (git, gh, slack, linear, ntn, gws, or one you register) as head words in the virtual terminal.
hust-open-atom-club/oh-dsh★ 325
Community distribution: TUI, desktop, and Web UI as one bundle with layered installation.
weijiafu14/pi2dsh★ 206
Pi Host ABI compatibility engine: after one install, unmodified Pi extensions from npm mount as native DSH plugins with `dsh plugin add <pi-package>`. Verified end to end on stock DSH with pi-mcp-adapter (full MCP manager: OAuth, resources, prompts, MCP Apps, elicitation, sampling), @tintinweb/pi-subagents, pi-code, pi-hermes-memory and pi-background-tasks; `pi2dsh inspect` reports a package's compatibility before installing.
lire1131/dsh-undo-savepoint★ 166
Undo/redo & rollback system for DSH: every config change is auto-snapshotted; undo/redo/restore to any version from the WebUI or the offline CLI/GUI tools (works even when DSH fails to boot).
Fishquito7/dsh-skill-mcp-panel★ 155
Manages DSH skills and MCP servers from the web settings: skill cards with hot enable/disable, workspace scopes, groups, batch migration and drag-and-drop import, plus stdio/HTTP MCP CRUD with connection tests, secret redaction and the unified dsh-panel CLI.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.