Skip to content

Latest commit

 

History

69 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dsh-update-copilot

License: MIT DSH core Zero build GitHub stars

An update copilot for DeepSeek Harness: tracks the dsh core, shipped bundles, and every installed plugin — merged package-centric across all profiles, with one-click updates for eligible independently owned installs.

The Update Copilot popup: the DeepSeek Harness core card collapsed, plugins merged across profiles into an Updates-available section with one-click update buttons, and the Up-to-date section folded away.

English | 中文

Why this exists

DSH moves fast, and so does its plugin ecosystem. Every profile installs plugins through pnpm specs — npm versions, GitHub commit pins, local link: checkouts — and each channel drifts out of date in its own way. Checking them by hand means walking every repo; auto-updating everything blindly means trusting third-party code with your environment.

This plugin takes the middle path: detect everything, summarize what changed, update only what you trigger. Updates are one click — no confirmation ceremony between you and the button — and target only the eligible profiles shown for that package. The DSH core is deliberately report-only — upgrading the harness restarts every session, so that decision stays with a human.

Features

🔭 Full radar dsh core + shipped bundles (dsh-base, dsh-web-app) + every profile's plugin dependencies, in one scan
🔄 Dual channel npm registry versions (full semver compare, prerelease-aware) and git upstreams (pinned-commit vs HEAD, link: checkouts via read-only ls-remote)
🧭 Update highlights Per-item: semver distance, risk level (major → high, minor → medium, patch → low), changelog material — npm versions between yours and latest, GitHub compare commits, release notes (with their body rendered inline), or local git log; every artifact links out (npm version pages, commits, releases, compare views) and every row carries a ↗ to its repository — monorepo sub-packages link to their subdirectory, npm plugins without a resolvable GitHub repository fall back to their npm package page
🤖 Agent tools update_copilot_scan / update_copilot_brief / update_copilot_update — ask your agent "any updates?" and get an honest, data-backed answer. Scans are package-centric (one row per package, merged across profiles); inferred mount relationships are presentation-only, while official packages are report-only and each direct dependency keeps its own update policy. profile is optional on brief/update: without it, a brief covers every eligible profile that has the package, and an update runs only across those eligible profiles
🖥 Web surfaces A sidebar trigger beside Settings (its badge hydrates on mount and refreshes after the startup scan; the badge can be turned off in settings for a quiet sidebar) opens a compact popup, and a ⚡ Update all quick button sits right beside it — no view needed: one click runs every outdated auto-updatable plugin in sequence (running shows an n/m readout and disables, completion flashes ✓/✗, failures or restart-required open the popup with details). Popup and full page share one folded layout — the DeepSeek Harness core card starts collapsed, and plugins split into an "Updates available" section plus a folded "Up to date" section (expand on click), inferred mount relationships staying with their parent. The full page lives on in Settings → Update Copilot, which also offers an opt-in Refresh every 30 minutes toggle (off by default — upstreams are only queried at startup and on your actions; when enabled, a forced background refresh runs every 30 minutes and the badge and open radar views follow along). Plugins are merged across profiles into one row per package, each installed profile's current → latest listed inline; mounted packages expand beneath their parent for disclosure while keeping independent ownership and update actions — a child Update targets only that child, a parent Update only the parent, and Update bundle updates only outdated eligible targets (outdated parent first, then outdated mounted children within each relationship's profile scope). One click on Update updates the package across its explicit eligible profiles, and a toolbar Update all runs every eligible outdated package in sequence (tick Auto-update on button click in Settings → Update Copilot and a click on the sidebar trigger starts that same pass automatically whenever outdated plugins are found; the dsh core stays report-only). All mutation actions share one UI operation lock; refresh is disabled until updates and refresh state settle. Updates stream live progress over SSE (resolving / downloading / retrying / stash / pull / restore phases) straight into a per-row progress bar. Updates are never silent — whoever started one (the auto-run, the agent tools, another tab), the sidebar badge turns into a pulsing updating dot and the popup/panel keep a live "Updating: <package>" banner across every seat, with update buttons disabled while one is running; whoever started the update, the matching row also renders the same live progress bar inline (the initiating row via its own SSE stream, every other seat mirrored from the 2-second status poll), and a new round for that package clears the row's stale previous result — a background update never collides with a foreground click into the confusing "another update is already running" error
🛡 Update guardrails Same-origin POST + explicit confirm, strict target allowlist, single-flight lock, 5-minute timeout; npm/github specs run only through the official dsh plugin CLI, link: checkouts update via git pull in their own directory (auto-stash → pull → restore; conflicts are always handed back for manual handling), while file: installs and official @deepseek-ai/* packages remain refused
🧯 Host export check Scans third-party plugins' named imports of @deepseek-ai/* against the current DSH host packages (and the target version when the core is behind). Peer ranges miss "range still matches, export is gone" — that class of error fails the whole plugin tree at boot. The core card and plugin rows surface the finding with a copy-pasteable cordis.patch.yml disable snippet and dsh plugin remove command. When DSH itself will not start, run node …/dsh-update-copilot/lib/cli.js (it does not boot a profile).
🌐 Fully bilingual Every user-facing string — panel, popup, badges, update highlights, recommendations, update errors — follows the UI language (zh/en); the agent tool path keeps stable English identifiers

Install

# from npm (recommended)
dsh plugin --profile web add dsh-update-copilot

# or straight from the GitHub repo
dsh plugin --profile web add github:hezhongtang/dsh-update-copilot

Restart dsh web, then open Settings → Update Copilot. Works the same in any other profile (--profile <name>).

Usage

Ask your agent

"check for updates"

The agent runs update_copilot_scan, then builds a brief for each outdated item and presents the risk before doing anything. Updates run only after you say yes — the update tool rejects calls without confirm: true.

Or use the popup / panel

The sidebar button beside Settings opens the compact radar popup (ESC or backdrop click closes; ?duc=1 in the URL opens it once — handy for screenshots and tests). The ⚡ Update all button right beside it runs the whole pass without opening anything: sequential updates for every outdated auto-updatable plugin, an n/m readout while running, ✓/✗ flash on completion, and the popup opening automatically when something failed or a restart is required. With Auto-update on button click enabled in Settings → Update Copilot, that same click also kicks off "Update all" the moment outdated plugins show up, with progress streaming right inside the popup.

Updates are visible from anywhere. Whoever started one — the sidebar auto-run, a row's Update button, the agent tool (update_copilot_update), another browser tab — while it is running the sidebar badge becomes a pulsing dot (tooltip names the package) when the popup is closed, and the popup/panel keep a live "Updating: <package> (<profile>)…" banner on top that follows the server-side stage (resolving / downloading / retrying / stash / pull / restore) and percentage. While the banner is up, every mutation action — per-row Update, the link→remote switch, Update bundle, toolbar Update all — is disabled, so a background update and a foreground click never fight over the single-flight lock and surface the "another update is already running" error. Sequential passes (Update all, Update bundle) render their queue inline: the executing row shows "Updating…" with a live progress bar, rows not reached yet flip their button to "Queued" (tooltip: it starts automatically when the current item finishes), and rows already attempted keep the regular disabled button so a stale "Update available" never reads as "not started"; when the pass finishes, the list refreshes itself to the new versions.

Settings → Update Copilot is the full page: core status (with a copyable upgrade command — never executed), every installed plugin merged across profiles into one row per package (each profile's current → latest listed inline), expandable mount relationships, inline update highlights, and a one-click Update button per row. A child row's Update affects only that child; a parent row's normal Update affects only that parent. Update bundle skips current targets, runs the outdated parent first when eligible, then each outdated selected mounted child in its relationship's profile scope, and reports progress/results. Each package carries explicit eligible profiles, so it never updates every same-named install blindly. A toolbar Update all button remains the separate global action, running every eligible outdated package in sequence. Live SSE progress drives a per-row progress bar. After an update, a plugin in the running profile is hot-reloaded in place when its entry and bundle patch are unchanged; the restart banner is only shown for updates outside the phase-1 hot-reload scope (bundle-patch changes, non-current profiles, self-update, etc.).

Agent tool reference

Tool Read/Write Purpose
update_copilot_scan read Full scan across core + all profiles, merged package-centric (10-min cache, force to bypass)
update_copilot_brief read Semver distance, risk, changelog material, recommendation for one package; optional profile restricts the brief to one profile, otherwise every profile that has the package contributes
update_copilot_update write Execute one confirmed update — without a profile, across its explicit eligible profiles only; npm/github specs through the official dsh plugin CLI (transient failures retry automatically — up to 3 attempts with jittered exponential backoff; deterministic errors like a missing version or refused auth fail fast); link: checkouts via git pull inside their own directory (auto-stash → pull → restore, conflicts handed back for manual handling), or with source: "remote" switch the dependency to the published npm version (or a github: spec when the package is not on npm) — breaking the local link

How it works

Every dependency spec is classified into a channel, and each channel has its own comparison. Scans merge the profiles' plugin lists package-centric: the same package installed in web, headless, and desktop appears once, carrying each profile's channel and versions. Within each profile, an active bundle whose contained patch mounts at least two production dependencies exposes presentation-only mount relationships; overlapping parents choose the largest verified child set, then package name. Mounting does not transfer update ownership: each direct dependency keeps its own update policy, and bundle updates use the selected relationship's profile scope. Local link: and file: dependencies stay local, and official @deepseek-ai/* packages remain report-only.

What counts as a plugin row: the packages a profile's manifest declares under dsh.profile.bundles (the list the host actually loads), plus every link: / file: checkout, which stays visible even before it is activated, plus the verified patch-mounted children of active bundles — their insert records are how the host loads those. Other plain dependencies in the manifest — a CLI or server runtime someone added for convenience — are not dsh plugins and don't render as one; that used to put a ghost update button beside the real plugin sharing the same GitHub repo. A manifest without a bundle list falls back to showing every dependency.

Channel Example spec Current Latest
npm ^0.1.4 installed package.json version newest version in the full registry doc
github github:owner/repo#sha pinned commit in pnpm-lock.yaml upstream HEAD via GitHub API
linked link:../my-plugin local git rev-parse HEAD git ls-remote origin HEAD (read-only)

The npm channel deliberately ignores the latest dist-tag: monorepo sub-packages often leave that tag stale, which false-flags installs that are actually newer than the tag. Versions are compared with full semver precedence (prereleases included), so 0.1.0-rc.6 > 0.1.0-rc.5 and 1.0.0 > 1.0.0-rc.1 both hold.

Updates execute through two vetted paths, never a raw shell string: npm/github specs run dsh plugin --profile <p> add <target> — the same path a human would type — with the target string validated against an allowlist; link: checkouts run git directly in their directory (git stash push for local changes → git pullgit stash pop to restore them). Transient failures are retried automatically: up to 3 total attempts, spaced by full-jitter exponential backoff (1s base, 8s cap) so a batch of updates doesn't re-hammer the registry in lockstep; deterministic failures — missing package/version (E404, ETARGET), refused auth (E401/403), git refusing outright (bad credentials, dubious ownership) — skip the remaining attempts and fail fast. Stalled git pulls abort themselves (http.lowSpeedLimit/http.lowSpeedTime) instead of hanging until the hard timeout. Merge conflicts or failed restores are never auto-resolved — the result reports attempts, a stash summary, and the last output. Child, parent, and bundle actions preserve the package row's explicit eligible profiles; global Update all remains separate and never updates every same-named dependency blindly.

A link: checkout can also be switched to a remote source: the copilot replaces the dependency spec with the newest published npm version (npm registry first), or with github:owner/repo#<origin HEAD> when the package has no npm release. The local link breaks and future updates follow the normal npm/github channel. Switching is destructive, so it always requires explicit confirmation and is never part of the default pull path.

When DSH will not start

If a third-party plugin import { aRemovedName } from '@deepseek-ai/…', the loader fails the whole plugin tree and the copilot UI never mounts. From any Node shell:

node ~/.dsh/profiles/web/node_modules/dsh-update-copilot/lib/cli.js
# linked install:
node /path/to/dsh-update-copilot/lib/cli.js

It checks the on-disk DSH only (no npm, no target-version preview), so it still runs when DSH will not boot. Exit 1 when the current host is already incompatible (prints disable / uninstall commands).

Security

  • The only mutating route is POST /dsh-update-copilot/update: same-origin and same-transport-scheme checks are enforced, with confirm: true required; forwarded scheme headers are not trusted. TLS-terminating proxies may set DSH_UPDATE_COPILOT_PUBLIC_ORIGIN to their public HTTP(S) origin; requests must match it exactly, and an invalid value fails closed.
  • Official @deepseek-ai/* packages and the dsh core are never auto-updated; the core's upgrade command is displayed, not run.
  • All upstream queries are read-only (registry.npmjs.org, api.github.com, git ls-remote) with hard timeouts; a failed check degrades that one item instead of failing the scan.

Limitations

  • Plugin hot reload is phase-1 scoped: it covers updates whose running profile entry still exists and whose new version keeps the same dsh.bundle.patch and dsh.client declaration (this includes link: checkouts — node_modules points at the checkout through a symlink, so the pull takes effect on the same files the reloader reads). Bundle-patch changes, non-current profiles, and copilot self-update still ask for a dsh restart.
  • link: updates require the checkout to have an upstream branch configured; uncommitted changes are auto-stashed and restored after the pull, and a failed restore (stash pop conflict) needs manual git stash list / git stash pop.
  • Switching a link: to a remote source breaks the local link and there is no automatic switch back (the spec must be edited by hand). The npm-first strategy installs the registry version, which may differ from your local development checkout.
  • Unauthenticated GitHub API is rate-limited (60 req/h) — briefs degrade gracefully to version lists.
  • Raw git+https:// specs are reported as-is without a comparison channel.
  • The host export check is static named imports: dynamic import(), APIs only reached at runtime, and import * as ns are not flagged. Official @deepseek-ai/* packages are skipped. The target pass only packs @deepseek-ai/dsh-* at the DSH version line; a failed pack or an independently versioned package (cordis, schemastery) is skipped rather than reported as incompatible.

Contributing

Issues and PRs welcome at hezhongtang/dsh-update-copilot. The codebase is intentionally small and dependency-free — plain ESM on the host, a hand-authored CJS bundle in the browser, no build step to set up.

License

MIT © 2026 hezhongtang

About

Update copilot for DeepSeek Harness: tracks the DSH core, bundled packages, and every installed plugin across npm and git, merged package-centric over all profiles, with one-click updates for eligible profiles. · DSH 更新助手:追踪 dsh 本体、bundle 包和所有已装插件(npm 与 git 双通道,跨 profile 按包合并),仅对符合条件的 profile 一键更新。

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages