fix(updater): resolve the dsh CLI through symlinks so one-click updates work on global installs #44

Closed
opened 2026-10-24 01:54:40 +00:00 by dsh · 1 comment
Owner

Symptom

On a normal global DSH install, the in-UI updater sees the newer version (0.8.0) but the install fails. The local panel shows the generic Plugin update failed; see server logs., and no new operation-* directory appears under <profile>/.plugin-manager/logs/ — i.e. dsh plugin add is never spawned.

Root cause

lib/plugin-updater.js#findDshCliEntry() resolves the running dsh entry with resolve(process.cwd(), process.argv[1]) and then walks parent directories looking for package.json with name: "@deepseek-ai/dsh".

Global installs launch DSH through a symlink chain, e.g. /usr/local/bin/dsh -> /opt/node/bin/dsh -> /opt/node-v24.../lib/node_modules/@deepseek-ai/dsh/lib/bin.js. process.argv[1] is the symlink path, resolve() does not follow it, so the walk starts in /usr/local/bin and finds no package.json. cliEntry is undefined, canAutoUpdate is false, and POST installExact() throws Automatic update is unavailable in this runtime. before spawning anything.

Reproduced on the deployment host (same symlink layout):

findDshCliEntry('/usr/local/bin/dsh')        => not found
findDshCliEntry('.../@deepseek-ai/dsh/lib/bin.js') => package found

The status GET already returns canAutoUpdate: false, but lib/client.js ignores it and still shows the Update button, so the user only sees a failed click.

Fix

  1. findDshCliEntry(): resolve the entry (and the candidate bin) with realpathSync before walking, so symlinked global installs are found. Keep the direct-path case working.
  2. UI: store canAutoUpdate from the status response; hide/disable the Update button when false and show a manual-install hint (dsh plugin --profile <profile> add <tarball>). A missing CLI is a permanent condition, not a transient error.
  3. Regression test with a temp package tree + symlink, plus an unresolved-path case.

Acceptance

  • findDshCliEntry(symlink) returns the real lib/bin.js for a fake @deepseek-ai/dsh package.
  • Status still reports canAutoUpdate; the UI does not offer an update it cannot perform.
  • With a resolvable CLI, the spawned command is node <real bin.js> plugin --profile <name> add <tarball>.

Layer

dsh-gitea plugin/tool (updater host + client). Found while testing 0.8.0 on a local install.

## Symptom On a normal global DSH install, the in-UI updater sees the newer version (0.8.0) but the install fails. The local panel shows the generic `Plugin update failed; see server logs.`, and no new `operation-*` directory appears under `<profile>/.plugin-manager/logs/` — i.e. `dsh plugin add` is never spawned. ## Root cause `lib/plugin-updater.js#findDshCliEntry()` resolves the running `dsh` entry with `resolve(process.cwd(), process.argv[1])` and then walks parent directories looking for `package.json` with `name: "@deepseek-ai/dsh"`. Global installs launch DSH through a symlink chain, e.g. `/usr/local/bin/dsh -> /opt/node/bin/dsh -> /opt/node-v24.../lib/node_modules/@deepseek-ai/dsh/lib/bin.js`. `process.argv[1]` is the symlink path, `resolve()` does not follow it, so the walk starts in `/usr/local/bin` and finds no package.json. `cliEntry` is `undefined`, `canAutoUpdate` is false, and POST `installExact()` throws `Automatic update is unavailable in this runtime.` before spawning anything. Reproduced on the deployment host (same symlink layout): ``` findDshCliEntry('/usr/local/bin/dsh') => not found findDshCliEntry('.../@deepseek-ai/dsh/lib/bin.js') => package found ``` The status GET already returns `canAutoUpdate: false`, but `lib/client.js` ignores it and still shows the Update button, so the user only sees a failed click. ## Fix 1. `findDshCliEntry()`: resolve the entry (and the candidate bin) with `realpathSync` before walking, so symlinked global installs are found. Keep the direct-path case working. 2. UI: store `canAutoUpdate` from the status response; hide/disable the Update button when false and show a manual-install hint (`dsh plugin --profile <profile> add <tarball>`). A missing CLI is a permanent condition, not a transient error. 3. Regression test with a temp package tree + symlink, plus an unresolved-path case. ## Acceptance - `findDshCliEntry(symlink)` returns the real `lib/bin.js` for a fake `@deepseek-ai/dsh` package. - Status still reports `canAutoUpdate`; the UI does not offer an update it cannot perform. - With a resolvable CLI, the spawned command is `node <real bin.js> plugin --profile <name> add <tarball>`. ## Layer dsh-gitea plugin/tool (updater host + client). Found while testing 0.8.0 on a local install.
dsh self-assigned this 2026-10-24 01:54:40 +00:00
dsh closed this issue 2026-10-24 01:56:28 +00:00
Author
Owner

Fixed in e43e4ca (PR #45).

Root cause confirmed on the deployment host's global layout: findDshCliEntry('/usr/local/bin/dsh') walked the symlink path (/usr/local/bin → /usr/local → /) and found no package.json, returning undefined. The updater then had cliEntry === undefined, canAutoUpdate: false, and failed before spawning dsh plugin.

Fix: realpath the entry and the candidate bin before comparing; findDshCliEntry('/usr/local/bin/dsh') now returns
/opt/node-v24.22.0-linux-x64/lib/node_modules/@deepseek-ai/dsh/lib/bin.js.
The UI now hides the Update button when canAutoUpdate is false and shows a manual-install hint; the host error also includes the exact dsh plugin --profile <name> add <tarball> command.

Tests: 143/143, leak scan + syntax clean. Released in 0.8.1 — because the currently installed 0.7.40-dsh.7 updater cannot detect the CLI either, 0.8.1 needs one manual tarball bootstrap, then the UI updater works for subsequent releases.

Fixed in `e43e4ca` (PR #45). Root cause confirmed on the deployment host's global layout: `findDshCliEntry('/usr/local/bin/dsh')` walked the symlink path (`/usr/local/bin` → `/usr/local` → `/`) and found no `package.json`, returning `undefined`. The updater then had `cliEntry === undefined`, `canAutoUpdate: false`, and failed before spawning `dsh plugin`. Fix: realpath the entry and the candidate `bin` before comparing; `findDshCliEntry('/usr/local/bin/dsh')` now returns `/opt/node-v24.22.0-linux-x64/lib/node_modules/@deepseek-ai/dsh/lib/bin.js`. The UI now hides the Update button when `canAutoUpdate` is false and shows a manual-install hint; the host error also includes the exact `dsh plugin --profile <name> add <tarball>` command. Tests: 143/143, leak scan + syntax clean. Released in 0.8.1 — because the currently installed 0.7.40-dsh.7 updater cannot detect the CLI either, 0.8.1 needs one manual tarball bootstrap, then the UI updater works for subsequent releases.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
dsh-plugins/dsh-gitea#44
No description provided.