dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Keep your DSH profile current: six compatibility checks against any core version, inspect a plugin before installing it, snapshot and restore the whole profile, update only what has a newer release, recheck the deferred list, and probe what really loads and answers — imports, apply(), route handlers, shadowed UI, dead endpoints. Menu, CLI, JSON.
Tools for upgrading the DeepSeek Harness core and the plugins of a profile.
Repository · MIT · runs against the DSH core you have installed — no core version is pinned (see Compatibility).
Runs as plain Python 3 (standard library only, no npm). The tool itself never needs Node to
start; it uses the node on PATH for two things — the runtime probe (verify) and parsing the
code an artifact ships — and where node is absent the parse is reported as not checked rather
than skipped silently. The read-only actions
(status, core-versions, plan, check, inspect) work against a running harness too —
they only read the profile, the installed core and the registry. Only a clean upgrade
(detach → core/plugin install → attach) needs DSH shut down, because the profile
directory is being rewritten.
Requirements: Python 3 and the dsh CLI on PATH for the actions that read or change the
profile (or DSH_INSTALL_DIR pointing at the installed core).
The script pulls in everything it needs for its checks itself: the npm registry over HTTP, the
marketplace index, and — for breaking-change analysis — a checkout of the core version
(git clone --depth 1 of the dsh-v<version> tag: one commit, because the comparison reads files
and never walks history). By default that checkout goes under the system temporary directory
(/tmp): the next run reuses it instead of downloading the same tag again, and the operating system
clears it on reboot. A directory set in the settings (or by --checkouts) is used instead.
The main idea is "detach everything, upgrade the core, install it all back, and set the incompatible ones aside into a list":
check ──► snapshot ──► detach all plugins ──► core upgrade ──► install compatible ones
└─► the rest into the list
No core version is pinned. Every command reads the core that is actually installed — found
through the dsh CLI on PATH, or named by DSH_INSTALL_DIR — and takes its facts from that core's
own bytes: the package manifests, the generated TYPERT faces that declare its wire endpoints, its
effective loader tree. Nothing assumes a release, so the installed core is always the baseline and
upgrading from an old one is the ordinary case rather than a special mode.
Verified end-to-end against three releases of the 0.1.x line:
| Installed core | Host packages | Client rows | TYPERT faces | Wire endpoints | status | check |
|---|---|---|---|---|---|---|
0.1.0-rc.8 | 193 | 43 | 7 | 26 | exit 0 | exit 0 |
0.1.1-rc.2 | 194 | 43 | 7 | 26 | exit 0 | exit 0 |
0.1.5-rc.2 | 237 | 55 | 15 | 84 | exit 0 | exit 0 |
Those numbers differ per release because they are read from that release, not assumed. A core whose surfaces the tool cannot interpret is reported as such — an empty inventory, a wire check that says it was not performed — rather than guessed at.
The installed core is read from either layout the harness can produce: a global install, whose
packages are nested under <core>/node_modules/@deepseek-ai, or a hoisted tree, where they sit
beside the core package. Reading only one of the two silently loses part of the inventory — and the
verdicts that depend on it.
One version-dependent input is worth knowing about: when no checkout of the target core is available,
the inline-purity rule (check 6) falls back to a built-in classification copied from 0.1.5-rc.2.
That set is deliberately broad, so a missing entry relaxes the check instead of inventing an
incompatibility; naming the target with --core and letting the tool fetch its checkout replaces the
built-in set with the target's own rule.
With no arguments the interactive menu opens — every action is reachable by clicking its number, and destructive ones are always shown in preview mode first:
git clone https://github.com/ThinkForge-core/dsh-upgrade-tools.git
cd dsh-upgrade-tools
python3 dsh_upgrade.py # menu: status, plan, check, detach, install, pipeline…
python3 scripts/menu.py # the same thing
The same tool also works as an ordinary CLI (scripts, cron):
# 1. Current state: core version, tags, installed plugins
python3 dsh_upgrade.py status
# 2. Which core versions exist at all and what happens to plugins on each
python3 dsh_upgrade.py plan
# 3. Full check of the chosen version (with a checkout and code scans)
python3 dsh_upgrade.py check --core 0.1.5-rc.2 --verbose
# 4. Full pipeline: snapshot → detach → (core upgrade) → install
python3 dsh_upgrade.py pipeline --core 0.1.5-rc.2 --yes
# 5. Or plugins only, leaving the core alone
# (updates the plugins that have a newer version; the rest are not touched)
python3 dsh_upgrade.py plugins --yes
# 5a. Including the plugins whose newer version is not confirmed for this core
python3 dsh_upgrade.py plugins --install-unknown --yes
# 6. Later: recheck the deferred ones and install the ones that became compatible
python3 dsh_upgrade.py recheck --install --yes
# 7. After an upgrade: do the installed copies actually load — and run?
python3 dsh_upgrade.py verify
dsh-upgrade — DSH core and profile plugin upgrade
core 0.1.1-rc.2 · profile web · plugins 16
target: auto — 0.1.5-rc.2 · mode: online · core directory: …/node_modules/@deepseek-ai/dsh
read-only — the profile is not touched
1 Status: core, tags, profile plugins — fast: cache and profile files only, nothing is executed
2 Core versions: what the registry offers
3 Plan: new core versions and what happens to plugins — quick, declarations only
4 Check: full compatibility matrix — checkout of the target + code scans
10 Incompatible list: show contents
11 Settings: profile, target, modes
12 CLI flag reference
13 Inspect: a plugin that is NOT installed yet — a directory or a .tgz; nothing is installed
14 Verify: do the installed plugins load — and do they DO anything? — imports each one, calls apply(), calls the route handlers it registered, reports shadowed surfaces
0 Exit
may change the profile
5 ! Snapshot and detach ALL plugins
6 ! Install plugins from a snapshot
7 Recheck the incompatible list
8 ! Update plugins that have a newer version (core untouched) — nothing else in the profile is touched
9 ! Full pipeline: detach → core → install
Tip: 4 — check before upgrading, 9 — the whole pipeline.
! mark and the red key flag the destructive ones.
Item 4 (check) is read-only: it reads the profile and writes the state files (the
incompatible list, the report, the cache), but it installs and detaches nothing.offline, "no clone", verbose output, colors, state directory and where the version
checkouts live. Every change is written to the settings file and applies to later runs
(see below).file:/link: specifiers are completed
after the prefix), arrows/Home/End edit the line, up-arrow recalls earlier paths. Quotes,
backslash escapes and ~ in what you typed are resolved before the path is used. Outside a
terminal the ordinary reader is used, so nothing changes for scripts.auto — <version> and is the newest published release
(prereleases included) — not the installed one. The installed core shows up in that line only
as a marked fallback (auto — 0.1.1-rc.2 (installed fallback)) when the registry cannot be
reached at all. The header resolves it from the registry cache, so it never blocks.--dry-run and ask for confirmation with the word
yes; the core upgrade (npm i -g) in the pipeline is only printed by default.--only, so one plugin can be detached or restored on its own. The
preview then shows exactly that selection. Item 8 asks two questions instead: whether to also
install a newer version that is not confirmed for this core (--install-unknown) and whether
to remove each plugin before installing its new version (--detach-first, off by default — the
new version is installed over the current copy). It needs no --only question because it only
ever touches the plugins that have a newer version; narrow it with --only on the command line
when you want a single plugin.cmd_*
functions as the CLI.apply(), and reports what each one registers — plus any
plugin whose UI host row is switched off (see
Do they actually work?). It also asks whether to run the --live probe
against the running DSH. Item 1 (status) shows both columns and the shadowed-surface section
from what is already known — the cached probe verdicts and the profile files — and never starts a
probe itself, so the overview stays fast; run item 14 to refresh the verdicts. That is also the
step that turns unconfirmed plugins into verified ones.dsh-upgrade — available actions:) and exits with code 0.Options are kept in a small JSON file, so a choice made once does not have to be retyped as flags on every run:
| Location | $DSH_UPGRADE_CONFIG, else $XDG_CONFIG_HOME/dsh-upgrade/config.json, else ~/.config/dsh-upgrade/config.json |
| Written by | the settings screen (menu item 11) — nothing else: a check, a plan or an upgrade never writes it |
| Priority | an explicit flag (that run only) > the environment > the saved file > the built-in default |
A flag given on the command line is never written back, so dsh_upgrade.py --profile other
stays a one-off. Item 11 → 10 ("forget saved settings") deletes the file and returns to the defaults;
item 11 → 9 removes the temporary checkouts right now.
Where the version checkouts live is one of those saved options:
| Value | Meaning |
|---|---|
temp (default) | <tmp>/dsh-upgrade-checkouts-<uid> — under the system temporary directory, so the OS clears it on reboot; reused between runs, and removable now with --prune-checkouts (menu item 11 → 9) |
keep | <DSH_HOME>/checkouts, kept between runs |
| a directory | anything you point at: --checkouts DIR, or menu item 11 → 8 |
A checkout is a means to an end (the comparison), so the default does not accumulate in a permanent
place. Whatever the setting, an existing checkout is reused — the configured location first, then
<DSH_HOME>/checkouts — and only cloned (with --depth 1) when neither has it. DSH_CHECKOUTS_ROOT
beats the saved setting (see Environment variables).
A checkout is not scratch — do not delete one by hand. It is the input of the comparison, and the next run reuses it instead of downloading the tag again; that is the whole reason the location is configurable and the reason
tempis a stable per-user path rather than a random directory.--prune-checkouts(menu item 11 → 9) is the only sanctioned deletion and it has to be asked for. Removing a checkout "to save space" silently turns the nextcheck/inspect/planinto a freshgit clone— and in a sandbox whose/tmpdoes not survive between commands, it can make the target unreachable for the rest of the session.
| Command | What it does |
|---|---|
menu | Interactive menu (the same one opens with no arguments). |
status [--verify] [--loader] [--summary] | Core version and directory, tags, recent versions, table of plugins with sources and loads + surface columns — whether the installed copy imports, and what it actually registers. The columns show the verdicts a previous verify left in the cache; without --verify nothing is executed, so the command is a fast read of the profile and the state files. --verify fills a missing or stale cache in; --loader prints the whole effective loader tree, marking the rows a shadowed plugin draws into; --summary prints one line of counts instead of the report. |
verify [--cached] [--live] [--no-handlers] [--web-url URL] [--loader] [--summary | --diff PATH] | Import the installed copy of every plugin with node, call its apply(), then call every route handler apply() registered once and read what it threw and logged — plus which plugins have a shadowed surface (their UI host row is switched off). --live also GETs every registered route on the running DSH (--web-url, default http://127.0.0.1:3080), which proves the row applied. --no-handlers skips the handler calls — a weaker answer, so it is not cached. --loader prints the effective loader tree with the shadowed rows marked. --summary prints one line of counts; --diff PATH prints only what changed since a result saved earlier. Writes state/verified-<profile>.json. |
core-versions | All core versions and tags, how many are newer than the installed one. |
plan [--limit N] [--all] | In a single run: all new core versions and what happens to plugins on each (fast, declarations only). |
check --core V [--update] [--summary] | Compatibility matrix: what happens to each plugin on version V. Reads the cached runtime verdicts to grade what the probe has proven on the installed core (see Verified); --update also queries the registry and marks the plugins that have a newer published version (see Available updates); --summary prints one line of counts instead of the matrix. Writes the incompatible list. |
inspect PATH [--core V] [--since OLD] | Compatibility of an artifact that is not installed yet — a plugin directory or a .tgz. Reads only that artifact; the profile is not read and nothing is installed. |
detach [--only NAME…] [--yes] | Snapshot of the whole profile and detaching of the plugins — all of them, or with --only just the named ones. Without --yes it only shows the plan and aborts. |
attach [--from F] [--only NAME…] [--update] [--install-unknown] [--prune-failed] [--yes] | Installation from a snapshot: installs the compatible ones, the rest go into the list. --only restores just the named plugins from the snapshot. |
recheck [--file F] [--install] [--install-unknown] [--yes] | Recheck the incompatible list and install the ones that became compatible; with --install-unknown also the unconfirmed ones (post-checked). |
plugins [--only NAME…] [--install-unknown] [--detach-first] [--yes] | Update the plugins that have a newer version, on the current core — all of them, or with --only just the named ones. Nothing else in the profile is touched: a plugin with nothing newer is not detached and not reinstalled (see Only the plugins with an update). --install-unknown also installs a newer version that is not confirmed for this core; --detach-first removes each plugin before installing its new version. |
pipeline [--core V] [--update] [--install-unknown] [--run-core-upgrade] [--yes] | Full pipeline from check to post-check. --install-unknown is off by default: only proven-compatible plugins are installed, everything else goes into the incompatible list. On Termux the core upgrade is followed by the patch layer before the plugins are installed — see On Termux. |
Wrapper scripts for each step live in scripts/ (menu.py, status.py, verify.py, plan.py,
check.py, artifact.py for inspect, detach.py, attach.py, recheck.py, pipeline.py,
plugins.py, cores.py); arguments are passed through. The wrapper for inspect is called
artifact.py because a file named inspect.py would shadow the stdlib inspect module for every
import in the tool.
Common flags: --profile (default web), --core, --offline, --no-clone,
--json, --verbose, --color auto|always|never, --state-dir,
--checkouts temp|keep|DIR, --prune-checkouts (delete the temporary checkouts and continue — or
exit, when no command was given), --termux / --no-termux and --termux-dir DIR (see
On Termux). They can be given both before
and after the subcommand — the defaults are applied after parsing, because argparse would
otherwise let a subparser's defaults overwrite a flag given before the subcommand.
The automatic upgrade target is the newest published core version. Everywhere --core is
optional (check, attach, recheck, pipeline), the tool resolves the highest published
version by semver, prereleases included, and prints it as
auto target: 0.1.5-rc.2 — the newest published version (use --core to pick another one).
The latest dist-tag is deliberately not used: on the real registry latest pointed at
0.1.5-rc.1 while 0.1.5-rc.2 was already published under next. The installed core is only a
fallback for when the registry cannot be reached at all; plan lists every candidate.
A target has to be a version the tool can obtain. A check compares plugins against a version, so
that version must be the installed core, a checkout already on disk, or one the registry publishes.
Anything else is refused with exit code 1 and a message naming what is published — a version
nobody published has no inventory to compare with, and reporting every plugin as "unconfirmed"
because of a typo would look like a result. --offline and --no-clone narrow what can be obtained,
not what is accepted: a version with no local copy is reported as unconfirmable rather than guessed at.
Any operation can be narrowed to some plugins. plugins, detach and attach take
--only NAME… and then touch exactly those plugins, leaving the rest of the profile as it is. Names
are matched exactly and a name the profile does not have is an error listing the installed ones, so a
typo cannot turn into a silent no-op. Two details follow from what a snapshot is: detach --only
still writes the snapshot of the WHOLE profile (a snapshot describes a state, not an operation), and
plugins --only leaves the incompatible list describing every plugin by rebuilding it from the
post-check. In the menu the same choice is a question before the preview, in items 5 and 6; item 8
only ever touches the plugins that have a newer version, so it asks about --install-unknown and
--detach-first instead.
Unconfirmed plugins are opt-in. A plugin with no declarations is neither proven compatible
nor proven broken, so pipeline, attach, recheck and plugins install it only with
--install-unknown (default: off). In the menu the same choice is a question with the [y/N]
default. Whatever is installed that way is re-judged afterwards by the post-check over the code
that actually landed, and the failures go into the incompatible list. For plugins that flag is
also what decides whether a newer but unconfirmed version may be installed; without it the
plugin is left exactly as it is and named in the Held back section, never dropped.
plugins is an update command, not a repair command: it touches only the plugins that have a
newer version, and a plugin with nothing newer is not detached, not reinstalled and not dropped.
A run therefore cannot leave a plugin out of the profile, and repairing a profile as a whole is what
detach followed by attach is for. Every name lands in exactly one of three groups, each printed
with its reason:
=== To update (2) ===
↑ dsh-alpha → 1.4.0
↑ dsh-read-url → 1.8.0 not confirmed for this core — installed because of
--install-unknown, checked afterwards
=== Held back — a newer version exists (1) ===
· dsh-beta 1.2.0 → 1.3.0: 1.3.0 is published but proven incompatible with this core
=== Left alone — nothing to update (1) ===
· dsh-gamma 0.19.1: no newer version is published
A newer version that evaluates compatible on the current core is installed by itself. A newer
version that is merely unconfirmed — the plugin declares no DSH version, or its declarations do
not admit this core — is held back and named, because installing it is a choice:
· dsh-read-url 1.7.0 → 1.8.0: 1.8.0 is published but not confirmed for this core
— pass --install-unknown to install it anyway, with a post-install code check
With --install-unknown it moves into the first group, is installed, and the post-check re-runs the
code checks over what actually landed; the verdict goes into the incompatible list. A version proven
incompatible is never installed, with or without the flag — it stays in the second group.
Only an npm source can have an update: a file:, link: or github: plugin is reinstalled from its
own specifier and has no registry version to compare with, so it is always in the third group. The
version chosen is pinned (name@version), so what the preview shows and what gets installed cannot
disagree — including when the installed copy itself is unconfirmed.
By default the new version is installed over the current copy, which is what dsh itself does
and what keeps a failed install from leaving the plugin missing. --detach-first restores the older
remove-then-install sequence; in the menu it is the second question of item 8.
On Termux/Android the core upgrade is not finished when npm i -g returns. That
command restores the pristine upstream tree, and the pristine tree does not run
there: Android's sepolicy denies link(2) in app-private storage (which is how the
write/edit tools publish a file, and how sessions and attachments are saved),
Bionic has no flock(2), sharp ships no android-arm64 binary, and several
process.platform === "linux" checks never match "android".
Those corrections live in a separate layer — the
deepseek-harness-termux
fork — whose anchor-based patcher is re-applied after every core install. The tool
knows about it: