dsh-plugin-upgrader 1.0.2 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,139 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.1 — 2026-10-27
4
+
5
+ - **The row no longer offers to switch the running version to itself.** A
6
+ path-installed bundle asked the registry for its version list but did not pass
7
+ its installed version along, so no row was marked as the one loaded. The row
8
+ the user is already on then drew a live "back to latest" button that pointed at
9
+ that same version. Two fixes: the installed version is passed through for path
10
+ installs too, and the button label no longer keys on "this is the latest
11
+ release" alone — a row that is both current and latest reads "switch", disabled.
12
+
13
+ ## 1.1.0 — 2026-10-27
14
+
15
+ The first published release since 1.0.2: the `1.0.3` entries below shipped in
16
+ this version, together with everything listed here.
17
+
18
+ - **Upgrading a theme plugin no longer leaves the page unstyled.** Replacing an
19
+ appearance plugin (`dsh-dream-skin`, `*-skin-*`, `dsh-client-ui-theme`) unloads
20
+ the code that injected its stylesheet, so the page snaps back to the host's
21
+ default look. The settings are NOT lost — `dsh-dream-skin` keeps them in
22
+ `$DSH_HOME/dream-skin.json`, outside the profile and written atomically, which
23
+ is why that file survives an upgrade untouched — but the injected CSS and the
24
+ `data-*` attributes on `<html>` lived only in the page, and the restarted
25
+ plugin has no way to put them back without being asked. A batch that actually
26
+ replaced such a plugin now says so and reloads the page once, after the plugin
27
+ has restarted, so the look comes back from that file. The reload is skipped for
28
+ failed steps, for steps that changed nothing, and for every ordinary plugin — a
29
+ failed upgrade must not also throw the page away.
30
+ - **The upgrader's own row explains itself, and can update itself.** Both halves
31
+ of "这个插件没法管理自己" were real, and they are different problems:
32
+ - *The row was blank.* Installing as `link:`/`file:` made the overview skip the
33
+ registry lookup entirely, so the row showed a version, an arrow with nothing
34
+ after it, and no history. A path install now still reads the published list
35
+ (the directory keeps its own version, which is what is actually loaded) and
36
+ says so, instead of looking like a package that was never published. It stays
37
+ non-upgradable: replacing a `link:` with a registry tarball is a different
38
+ install, not an upgrade.
39
+ - *It could not update itself.* A version change through the batch job starts
40
+ with `removeBundle`, and removing this bundle unloads the very code that
41
+ would run the install that follows — the batch dies with the plugin that was
42
+ driving it, leaving the profile without it. The row now offers the page's own
43
+ installer instead (`plugins.tab` owner prop `install`), which is owned by the
44
+ plugin manager and therefore survives the unload. The batch and "Select all"
45
+ skip this row for the same reason, so one click can no longer strand the
46
+ profile.
47
+ - **The version list no longer silently disappears.** Several plugins drew no
48
+ history button and no explanation, which looked like "this package has no
49
+ published versions" — it was a failed registry lookup being swallowed. Three
50
+ separate causes, all measured against `registry.npmmirror.com`:
51
+ - **The tab asked for every package at once.** Eleven concurrent connections
52
+ timed out at the 10 s deadline while the same eleven asked one at a time
53
+ answered in under 1.2 s. Lookups now go through a four-at-a-time cap.
54
+ - **The documents are large.** `billion-context` is a 26 MB packument and
55
+ 10 MB even abbreviated, so a healthy request could lose a 10 s race on its
56
+ own. Lookups now ask for npm's abbreviated form
57
+ (`application/vnd.npm.install-v1+json`, which keeps `time`, `engines` and
58
+ `deprecated`), the deadline moved to 20 s, and a transport failure is
59
+ retried once — a `UND_ERR_CONNECT_TIMEOUT` is socket contention, not a
60
+ verdict about the package.
61
+ - **The failure was invisible.** A row whose lookup failed is now drawn with
62
+ the reason (`版本列表读取失败,点「检查更新」可重试`) instead of looking
63
+ like a package with no history. A failed lookup is also cached for a shorter
64
+ window than a successful one, so one bad render does not make the next
65
+ mount slow too.
66
+ - **A mirror's stale `latest` tag no longer turns an upgrade into a downgrade.**
67
+ npmmirror serves `@deepseek-ai/dsh-subagent-codex` with `latest: 0.0.1-rc.1`
68
+ while the profile runs `0.2.1-rc.3`, so the row claimed the installed build
69
+ was newer than latest and offered a downgrade as an upgrade. A tag that sorts
70
+ below the installed version is now treated as broken and recovered from the
71
+ published list, preferring a stable release over a prerelease. A prerelease
72
+ that merely sorts above a healthy tag still does not steal `latest`.
73
+ - **An upgrade no longer dies on `bundle-in-use`.** Replacing a version means
74
+ remove + install, and `removeBundle` only unloads a bundle it reads as enabled.
75
+ A plugin that is disabled but still holds a live loader fiber therefore went
76
+ straight to the "in use" check and was refused, which made every upgrade of a
77
+ disabled plugin fail. The removal now clears that fiber and retries once.
78
+ - **The unload asks the right question.** 1.0.3's follow-up assumed
79
+ `setBundleEnabled(name, false)` reloads unconditionally, so it would dispose the
80
+ fiber. It does not: `selectBundle` returns early when the bundle is already
81
+ deselected, so the manifest does not change, so `reconcileProfile` has no
82
+ dropped row to dispose and the fiber survives — the retry then failed for
83
+ exactly the same reason. The removal now reads `enabled` first and, for a
84
+ bundle the manager already reads as disabled, re-selects it and deselects it
85
+ again, which is the only form of the toggle that produces a change a live
86
+ process can act on.
87
+ - **A refusal the host cannot fix says so.** A second `bundle-in-use` now comes
88
+ back with the manager's code *and* a bilingual diagnostic ("the loader still
89
+ holds its plugin row; restart DSH and upgrade again"), and the tab shows the
90
+ diagnostic instead of the bare code. Retrying in the same process cannot clear
91
+ it, so the panel has to name the one action that does. The re-select is also
92
+ undone on the way out, so a failed upgrade never leaves the plugin enabled.
93
+ - **The primary button follows the theme.** Its label colour comes from
94
+ `--dsw-alias-label-primary-inverted` instead of a hard-coded white, so it stays
95
+ readable when the host theme uses a light primary fill.
96
+ - **Tests**: `test/install.test.ts` covers the replacement path (the remove +
97
+ install fallback and its fresh request id, the unload retry, the
98
+ already-disabled re-select including a refused one, the enabled-state restore,
99
+ and the restart diagnostic on both refusal spellings); `test/registry.test.ts`
100
+ covers the abbreviated request, the single retry, the shorter failure cache and
101
+ the 404-is-not-contention rule; `test/overview.test.ts` fails if a page load
102
+ stops respecting the cap; `test/shape-packument.test.ts` pins the stale-tag
103
+ rule. `scripts/verify-overview-live.mjs` runs the built host half against the
104
+ real mirror.
105
+
106
+ ## 1.0.3 — unreleased as its own version
107
+
108
+ These changes were staged for 1.0.3, which was never published on its own; they
109
+ first shipped in 1.1.0.
110
+
111
+ - **Upgrades report what pnpm is doing.** The host subscribes to
112
+ `plugin-manager/install-log` and `plugin-manager/install-state`, so pnpm's own
113
+ output ("Verifying lockfile against supply-chain policies", `ETIMEDOUT` retries,
114
+ registry fallbacks) now streams into the job log while the step runs, instead of
115
+ only surfacing after it ends. An upgrade that sits for five minutes on the mirror
116
+ retry ladder finally looks like what it is.
117
+ - **The progress bar is honest about long steps.** While a step is in flight the
118
+ panel shows a live elapsed clock, the phase (`installing` / `applying` /
119
+ `cancelling`), the registry attempt when a fallback is in progress, and an
120
+ indeterminate bar. `done/total` only moves when a step really finishes, which
121
+ previously left it pinned at 0% for minutes.
122
+ - **Cancel works at every stage.** The second install of a remove + install used to
123
+ pass `requestId: undefined`, so that whole stretch was uncancellable; each install
124
+ now mints its own id and the job reports it back. Cancelling also explains what
125
+ happened: settled already, past the point of no return, or stopping now.
126
+ - **The heartbeat is a timer (2 s) instead of a per-step write.** A single step over
127
+ the 15-minute stale window was declared abandoned mid-work, which both killed a
128
+ healthy job and let a second batch start against the same profile.
129
+ - **A failed install no longer tries to restore the enabled state** of a package it
130
+ never added, which used to report a second `operation-error` on top of the real
131
+ failure.
132
+ - **The panel no longer sticks after a batch settles.** `submitJob` resolves only
133
+ after the whole batch finished, and the tab wrote a fresh `queued` snapshot over
134
+ the result, leaving the spinner and the disabled buttons behind until the tab was
135
+ remounted.
136
+
3
137
  ## 1.0.1 — 2026-10-23
4
138
 
5
139
  - **Rewritten in TypeScript.** The whole project is now typed; `pnpm run typecheck`
package/README.en.md CHANGED
@@ -18,6 +18,9 @@ English · [中文](./README.md)
18
18
 
19
19
  - **One-click upgrade** — bump every upgradable plugin to its newest version, with
20
20
  progress, per-plugin results and failure reasons.
21
+ - **Progress you can actually see** — the raw pnpm output (registry retries, lockfile
22
+ verification), a live elapsed clock per step, an indeterminate bar while a step
23
+ runs, and a cancel button that works.
21
24
  - **Multi-select upgrade** — tick any subset of plugins and upgrade only those.
22
25
  - **Version history / rollback** — expand any plugin to see every published version
23
26
  (with publish date and engine requirement) and switch to it in one click.
@@ -75,8 +78,8 @@ host half:
75
78
  | --- | --- |
76
79
  | `GET /api/plugin-upgrader/overview` | installed plugins plus their published versions (registry packument, 5-minute cache, 10-second timeout) |
77
80
  | `POST /api/plugin-upgrader/job` | queue a batch of upgrade / switch steps, return a job id immediately |
78
- | `GET /api/plugin-upgrader/job?id=…` | poll progress and per-plugin results |
79
- | `POST /api/plugin-upgrader/cancel` | abort the step in flight |
81
+ | `GET /api/plugin-upgrader/job?id=…` | poll progress, current phase, elapsed time, per-plugin results and the live log |
82
+ | `POST /api/plugin-upgrader/cancel` | abort the step in flight, including the second install of a remove + install |
80
83
 
81
84
  Every route is loopback-only: a batch installs packages, so a paired remote device
82
85
  or a cross-site page must never reach it.
@@ -94,6 +97,29 @@ Removing a plugin drops its enabled / disabled state, so each step re-applies
94
97
  `setBundleEnabled` with the state it had before the upgrade. A disabled plugin stays
95
98
  disabled after being upgraded.
96
99
 
100
+ ### Why an upgrade sometimes takes minutes
101
+
102
+ Not this plugin: pnpm. Before installing, pnpm 11 re-checks every lockfile entry
103
+ against the registry (`Verifying lockfile against supply-chain policies`), and a
104
+ mirror such as `registry.npmmirror.com` answers `ETIMEDOUT` for plenty of packages,
105
+ which starts a 10-second → 1-minute retry ladder. Upgrading an already-installed
106
+ package additionally costs two pnpm runs (remove, then install).
107
+
108
+ The old build showed nothing during that time and moved the bar once per completed
109
+ step, so it read as a hang. The host now subscribes to
110
+ `plugin-manager/install-log` and `plugin-manager/install-state` and streams pnpm's
111
+ own output, the phase and the retry count into the job log, while the panel shows an
112
+ elapsed clock and an indeterminate bar — a slow install looks like a retrying one
113
+ instead of a frozen 0%.
114
+
115
+ Two related bugs fixed with it:
116
+
117
+ - The heartbeat is a timer (2 s), no longer a write per completed step. A single step
118
+ lasting over 15 minutes used to be declared abandoned, cancelling a healthy job and
119
+ letting a second batch start on the same profile.
120
+ - The second install of a remove + install passed `requestId: undefined`, making that
121
+ stretch uncancellable. Every install now has its own id, so cancel works throughout.
122
+
97
123
  ### One batch at a time
98
124
 
99
125
  The host runs one job at a time. A second submission gets `409 busy` with the id of
@@ -104,7 +130,7 @@ error:
104
130
  the running batch
105
131
  - `overview` carries an `active` field, so a freshly mounted tab reconnects to work
106
132
  in progress
107
- - a job writes a heartbeat every few seconds; a job left behind by a host restart is
133
+ - a job re-stamps a heartbeat every 2 seconds; a job left behind by a host restart is
108
134
  declared abandoned once the heartbeat goes stale, so it cannot block every later
109
135
  upgrade with a permanent `busy`
110
136
 
package/README.md CHANGED
@@ -15,6 +15,8 @@ DSH 自带的插件页能装、能卸、能看已安装列表,但缺两件事
15
15
  ## 功能
16
16
 
17
17
  - **一键升级** —— 一次把全部可升级插件升到最新版,带进度、逐项结果和失败原因。
18
+ - **看得见的进度** —— 升级时直接显示 pnpm 的原始输出(含换源重试、lockfile 校验),
19
+ 每步都有已用时计时和不确定态进度条,卡住时可以一键取消。
18
20
  - **多选升级** —— 勾选任意几个插件,只升级选中的这些。
19
21
  - **版本历史 / 回滚** —— 展开任意插件即可看到已发布的全部版本(含发布时间与引擎要求),点一下切到那个版本。
20
22
  - **一键跳 GitHub** —— 每行的 `GH` 按钮打开插件仓库主页;点版本号直接进该版本的 GitHub 发布页。
@@ -67,8 +69,8 @@ dsh plugin --profile web add "$PWD"
67
69
  | --- | --- |
68
70
  | `GET /api/plugin-upgrader/overview` | 已安装插件 + 各插件已发布版本(registry packument,5 分钟缓存,10 秒超时) |
69
71
  | `POST /api/plugin-upgrader/job` | 提交一批升级 / 切换步骤,立即返回 job id |
70
- | `GET /api/plugin-upgrader/job?id=…` | 轮询进度与逐项结果 |
71
- | `POST /api/plugin-upgrader/cancel` | 中断当前步骤 |
72
+ | `GET /api/plugin-upgrader/job?id=…` | 轮询进度、当前阶段、已用时、逐项结果与实时日志 |
73
+ | `POST /api/plugin-upgrader/cancel` | 中断当前步骤(含「先卸载再安装」的第二次安装) |
72
74
 
73
75
  所有路由仅限本机回环访问:批量操作会安装软件包,配对设备或跨站页面一律拒绝。
74
76
 
@@ -83,6 +85,25 @@ dsh plugin --profile web add "$PWD"
83
85
  卸载会丢掉插件原有的启用 / 禁用状态,因此每一步在安装完成后会按操作前的状态
84
86
  重新 `setBundleEnabled`,禁用中的插件升级后仍然是禁用状态。
85
87
 
88
+ ### 为什么升级有时候要好几分钟
89
+
90
+ 不是这个插件慢,是 pnpm 慢。pnpm 11 在 `install` 前会拿锁文件里的每个条目去 registry
91
+ 重新核对一次(`Verifying lockfile against supply-chain policies`),国内镜像
92
+ (`registry.npmmirror.com`)对不少包会 `ETIMEDOUT`,于是走 10 秒 → 1 分钟的重试阶梯。
93
+ 一次「升级」如果目标是已安装的包,还是 remove + install 两条 pnpm 命令,两段各等一次。
94
+
95
+ 旧版本在这里什么都不显示,进度条只在「整步完成」时动一格,所以看起来就是死住了。
96
+ 现在宿主端订阅了 `plugin-manager/install-log` 和 `plugin-manager/install-state`,
97
+ 把 pnpm 的原始输出、当前阶段、重试次数实时转进 job 日志,界面同时显示已用时和
98
+ 不确定态进度条:慢的时候你能看见它在重试,而不是盯着一个不动的 0%。
99
+
100
+ 顺带修掉的两个坑:
101
+
102
+ - 心跳改成定时器(2 秒),不再按「完成一步」写。单步超过 15 分钟以前会被误判成
103
+ 「job 已放弃」,于是一个健康的任务被作废,还能再并进来一批。
104
+ - 「先卸载再安装」的第二次安装以前用 `requestId: undefined`,导致那一段无法取消;
105
+ 现在每次安装都有自己的 id,取消在任何阶段都有效。
106
+
86
107
  ### 同一时刻只跑一批
87
108
 
88
109
  宿主端一次只允许一个 job。重复提交会拿到 `409 busy`(带正在跑的那个 job id),
@@ -90,7 +111,7 @@ dsh plugin --profile web add "$PWD"
90
111
 
91
112
  - 新开一个插件页标签、或在进度跑着时再点「一键升级」,都会看到正在进行的那一批
92
113
  - `overview` 里带 `active` 字段,标签页一挂载就能接上正在跑的任务
93
- - job 每隔几秒写一次心跳时间戳,宿主重启后遗留的「僵尸 job」会按心跳超时自动作废,
114
+ - job 每 2 秒刷一次心跳时间戳,宿主重启后遗留的「僵尸 job」会按心跳超时自动作废,
94
115
  不会永久占着位置让之后所有升级都撞 `busy`
95
116
 
96
117
  主进程重启会让正在跑的 job 中断,界面会把这种情况显示为失败而不是永远转圈。