fix(updates): report a plugin build installed beside the running one, never "up to date" on a failed check

A `claude plugin update` run from a terminal (or with only the Claude desktop
app open) unpacks the new build and starts nothing: no SessionStart fires, so
the dashboard kept running the old cache dir across reboots, and with the
forge unreachable the sidebar read "Up to date" (seen on a machine stuck at
1.8.10).

- pluginStatus asks supersededBy for its own root: a recorded sibling install
  sets `installed_not_running: {root, version}` and `update_available`, with
  no network needed
- the sidebar reads "<version> installed, not running"; the update modal keys
  on it, so Update now is offered even with no release tag or sha
- plugin-update-helper hands over to the recorded install first, the newest
  sibling dir only as a fallback
- a plugin install whose release lookup failed reads "Could not check for
  updates" instead of "Up to date"
- docs/PLUGINS.md: the case and the manual hand-over command
This commit is contained in:
2026-10-01 10:09:12 +07:00
parent 969b06253c
commit 058c574138
14 changed files with 208 additions and 27 deletions
+24 -8
View File
@@ -415,6 +415,18 @@ async function pluginStatus(root, pluginInstall, options = {}) {
const comparable = release.ok && Boolean(localVersion);
const releaseIsNewer = comparable && compareVersions(release.tag, localVersion) > 0;
// `claude plugin update` run from a terminal unpacks the new build into a
// sibling cache dir and starts NOTHING — only a SessionStart or this
// dashboard's own update chain hands the server over. Without a Claude Code
// session this server ran the old build forever, and with the forge
// unreachable it said "up to date". The local record needs no network, and
// "Update now" already re-bootstraps from the newest sibling.
const installedRoot = require("../../scripts/plugin-bootstrap").supersededBy(root);
const installedNotRunning = installedRoot
? { root: installedRoot, version: readLocalVersion(installedRoot) }
: null;
const updateAvailable = releaseIsNewer || Boolean(installedNotRunning);
const manualParts = [
`claude plugin marketplace update ${pluginInstall.marketplace}`,
`claude plugin update ${pluginInstall.plugin}@${pluginInstall.marketplace} -y`,
@@ -425,7 +437,8 @@ async function pluginStatus(root, pluginInstall, options = {}) {
// `install_kind`, not on this, or they will report "check manually" for an
// install the dashboard can in fact check AND update.
git_repo: false,
update_available: releaseIsNewer,
update_available: updateAvailable,
installed_not_running: installedNotRunning,
repo_root: root,
situation: "plugin_managed",
situation_note:
@@ -438,13 +451,16 @@ async function pluginStatus(root, pluginInstall, options = {}) {
// gets the same sentence. Left null, `agenthub update-check` printed the
// literal "null" as its headline on every plugin-cache install — the one
// install kind that cannot fall back to a commit count.
message: releaseIsNewer
? `Release ${release.tag} is available (this install reports ${localVersion || "no version"}).`
: comparable
? `Running the newest release (${release.tag}).`
: release.ok
? `This install reports no version, so it cannot be compared with release ${release.tag}.`
: `Could not read the latest release (${release.reason}); version comparison unavailable.`,
message: installedNotRunning
? `Version ${installedNotRunning.version || "(unknown)"} is installed but not running ` +
`(this server runs ${localVersion || "no version"}); "Update now" switches the dashboard to it.`
: releaseIsNewer
? `Release ${release.tag} is available (this install reports ${localVersion || "no version"}).`
: comparable
? `Running the newest release (${release.tag}).`
: release.ok
? `This install reports no version, so it cannot be compared with release ${release.tag}.`
: `Could not read the latest release (${release.reason}); version comparison unavailable.`,
install_kind: "plugin",
plugin_marketplace: pluginInstall.marketplace,