dsh-plugin-upgrader 1.6.3 → 1.7.0
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 +53 -0
- package/README.en.md +37 -1
- package/README.md +29 -0
- package/lib/client.js +55 -8
- package/lib/host/install.d.ts +19 -1
- package/lib/host/install.d.ts.map +1 -1
- package/lib/host/install.js +166 -8
- package/lib/host/install.js.map +1 -1
- package/lib/host/overview.d.ts +19 -0
- package/lib/host/overview.d.ts.map +1 -1
- package/lib/host/overview.js +27 -3
- package/lib/host/overview.js.map +1 -1
- package/lib/host/registry.d.ts +50 -1
- package/lib/host/registry.d.ts.map +1 -1
- package/lib/host/registry.js +63 -2
- package/lib/host/registry.js.map +1 -1
- package/lib/host/runtime.d.ts +13 -0
- package/lib/host/runtime.d.ts.map +1 -0
- package/lib/host/runtime.js +114 -0
- package/lib/host/runtime.js.map +1 -0
- package/lib/host/version.d.ts +22 -3
- package/lib/host/version.d.ts.map +1 -1
- package/lib/host/version.js +278 -3
- package/lib/host/version.js.map +1 -1
- package/lib/index.d.ts +78 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +91 -14
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.7.0 — 2026-11-03
|
|
4
|
+
|
|
5
|
+
- **Every version is judged against the DSH that is actually running, before it is
|
|
6
|
+
offered.** The host refuses an install whose declared DSH range does not admit this
|
|
7
|
+
runtime, and on the older replacement path it refuses *after* the bundle has been
|
|
8
|
+
unloaded — switching a real skin plugin to `0.1.5` (`>=0.1.7-rc.1 <0.1.8-0`) left the
|
|
9
|
+
package removed from the profile with a dangling patch id. Both directions are covered
|
|
10
|
+
now. Going *down* the history on a newer DSH marks the releases that no longer admit
|
|
11
|
+
this runtime and disables their switch button. Going *up* on an older DSH stops
|
|
12
|
+
offering `latest` as an upgrade when that release does not admit this runtime, and the
|
|
13
|
+
row says which range it wants instead.
|
|
14
|
+
- **The runtime version comes from the host's own entry point.** A profile's
|
|
15
|
+
`@deepseek-ai/dsh-app-boot` is a link into an npx cache and measured `0.1.2-rc.1`
|
|
16
|
+
against a runtime that reports `0.2.1-rc.5`; reading the profile would have marked
|
|
17
|
+
installable releases uninstallable. When the version cannot be read, nothing is marked
|
|
18
|
+
— the host stays the judge, and the page only loses a hint.
|
|
19
|
+
- **The rules are the gate's rules.** `engines.dsh` decides alone when present;
|
|
20
|
+
otherwise every `peerDependencies` range on `@deepseek-ai/dsh` and `@deepseek-ai/dsh-*`
|
|
21
|
+
must hold, with `workspace:^`/`~`/`*` meaning "this runtime", prereleases included.
|
|
22
|
+
`src/host/version.ts` implements the matching, checked against `node-semver` over 49
|
|
23
|
+
ranges × 41 versions (2035 pairs, all agreeing). Three inequalities were wrong before
|
|
24
|
+
that check — most visibly `~0.1.7-rc.1` admitting `0.1.6`.
|
|
25
|
+
- **A batch no longer disappears when the plugin replaces itself.** Upgrading another
|
|
26
|
+
plugin is what makes the host re-stage this plugin's own module in a `live` profile, so
|
|
27
|
+
the install a user was watching could evict the code tracking it: the replacement
|
|
28
|
+
instance started with an empty job map, answered `unknown-job` for the id the page was
|
|
29
|
+
polling, and the page reported "lost contact" over an install that finished fine. The
|
|
30
|
+
batch store now lives on `globalThis` under a stable symbol, so whichever module
|
|
31
|
+
instance answers knew the batch. Cleanup only clears the run that owns it.
|
|
32
|
+
- **A version the host would refuse is never installed, and a failed replacement puts the
|
|
33
|
+
old one back.** The page already marked those releases, but a client-side mark cannot
|
|
34
|
+
stop a request, and the host's gate fires after pnpm has rewritten the lockfile — which
|
|
35
|
+
is how a refused switch used to end with the package gone. The step now reads the
|
|
36
|
+
target's declared range *before* touching anything and answers
|
|
37
|
+
`incompatible-version` with the range it wanted, leaving the profile untouched. When a
|
|
38
|
+
failure slips past that (the gate is still the final word), the recovery distinguishes
|
|
39
|
+
the two damages instead of guessing: a package that is still on disk gets re-selected, a
|
|
40
|
+
package that is gone gets the previous version installed again. The outcome carries
|
|
41
|
+
`rolledBack` so the log can say what came back, and a rollback that is itself refused
|
|
42
|
+
says so rather than reporting a clean failure over a missing plugin.
|
|
43
|
+
- **The two ways a restart wait ends badly now say different things.** A pid that never
|
|
44
|
+
changes means the old process is still answering; a pid that *did* change but a page that
|
|
45
|
+
cannot reload onto the new host means another restart. Both used to print the first
|
|
46
|
+
sentence, which sent people looking for a process that was already gone.
|
|
47
|
+
- **One unanswered poll no longer reports a healthy upgrade as lost.** Polling a batch used
|
|
48
|
+
to stop at the first failed request, and the tab reads "polling stopped" as "the host
|
|
49
|
+
restarted" — so an install that went on to succeed could still finish with a "lost
|
|
50
|
+
contact" banner. The host answers from the same thread that runs the install, so a
|
|
51
|
+
single poll timing out or coming back 5xx during pnpm is ordinary, not fatal. `fetch`
|
|
52
|
+
rejecting (a keep-alive socket closed under a long install) leaked out of the loop
|
|
53
|
+
entirely for the same reason. Polls now retry up to four times in a row, and a good poll
|
|
54
|
+
resets the count, so it takes ~3 s of real silence to give up.
|
|
55
|
+
|
|
3
56
|
## 1.6.3 — 2026-11-02
|
|
4
57
|
|
|
5
58
|
- **"Restart DSH" is now a thing the page can walk you through, instead of advice it
|
package/README.en.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/dsh-plugin-upgrader)
|
|
4
4
|
[](./LICENSE)
|
|
5
|
+
[](https://www.dsh.so/artifact/dsh-plugin-upgrader/)
|
|
6
|
+
[](https://www.dsh.so/artifact/dsh-plugin-upgrader/)
|
|
7
|
+
[](https://dsh.market/)
|
|
8
|
+
[](https://deepseekharnessplugins.com/plugins/zuwenyin/dsh-plugin-upgrader)
|
|
5
9
|
|
|
6
10
|
> One-click upgrades plus version history and rollback for the DeepSeek Harness
|
|
7
11
|
> **Plugins** page.
|
|
@@ -25,7 +29,10 @@ English · [中文](./README.md)
|
|
|
25
29
|
runs, and a cancel button that works.
|
|
26
30
|
- **Multi-select upgrade** — tick any subset of plugins and upgrade only those.
|
|
27
31
|
- **Version history / rollback** — expand any plugin to see every published version
|
|
28
|
-
(with publish date and engine requirement) and switch to it in one click.
|
|
32
|
+
(with publish date and engine requirement) and switch to it in one click. Every
|
|
33
|
+
version is first checked against the **DSH you are actually running**: releases
|
|
34
|
+
this runtime cannot install are marked and their switch button is disabled (the
|
|
35
|
+
host's gate refuses them only after the bundle has been unloaded).
|
|
29
36
|
- **GitHub links** — the `GH` button opens the plugin's repository; clicking a
|
|
30
37
|
version number opens that version's GitHub release page.
|
|
31
38
|
- **A lagging mirror is named** — versions published upstream but not yet mirrored
|
|
@@ -309,6 +316,35 @@ acceptance. `dshOperations` (install / start / uninstall / rollback) follows the
|
|
|
309
316
|
rule: left out rather than filled with `passed` before there is a per-release run behind
|
|
310
317
|
it.
|
|
311
318
|
|
|
319
|
+
### Whether each version matches the DSH you run
|
|
320
|
+
|
|
321
|
+
DSH checks a plugin's declared DSH requirement at install time and **refuses** the install
|
|
322
|
+
when it does not hold. That check happens after the version has been taken out on the
|
|
323
|
+
older replacement path, which is how "I clicked switch and the plugin disappeared" really
|
|
324
|
+
happened. The page therefore reaches the verdict first, under exactly the host's rules:
|
|
325
|
+
|
|
326
|
+
- when the manifest declares `engines.dsh`, that range alone decides and
|
|
327
|
+
`peerDependencies` are ignored;
|
|
328
|
+
- otherwise every `peerDependencies` range on `@deepseek-ai/dsh` and
|
|
329
|
+
`@deepseek-ai/dsh-*` must hold, with `workspace:^` / `~` / `*` meaning "this very
|
|
330
|
+
runtime";
|
|
331
|
+
- prereleases take part in matching (`includePrerelease`), as they do at the gate.
|
|
332
|
+
|
|
333
|
+
Both directions are covered: after DSH moves forward, releases in the history that no
|
|
334
|
+
longer admit this runtime are marked "not for this DSH" and their switch button is
|
|
335
|
+
disabled; while DSH stays behind, a `latest` that this runtime cannot install stops
|
|
336
|
+
counting as an upgrade, and the row says which range it wants instead.
|
|
337
|
+
|
|
338
|
+
The runtime version is read from **the host's own entry point**, not from the profile: a
|
|
339
|
+
profile's `@deepseek-ai/dsh-app-boot` is a link into an npx cache and measured
|
|
340
|
+
`0.1.2-rc.1` against a runtime that reports `0.2.1-rc.5`. When it cannot be read, nothing
|
|
341
|
+
is marked — a guessed version would call installable releases uninstallable.
|
|
342
|
+
|
|
343
|
+
The matcher is implemented in `src/host/version.ts` and was checked against `node-semver`
|
|
344
|
+
over 49 ranges × 41 versions (2035 pairs, all agreeing). The `semver` package is not
|
|
345
|
+
pulled in because one more runtime dependency is one more supply-chain review, and the
|
|
346
|
+
only semantics needed here are the `includePrerelease` ones.
|
|
347
|
+
|
|
312
348
|
## Project layout
|
|
313
349
|
|
|
314
350
|
```
|
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/dsh-plugin-upgrader)
|
|
4
4
|
[](./LICENSE)
|
|
5
|
+
[](https://www.dsh.so/artifact/dsh-plugin-upgrader/)
|
|
6
|
+
[](https://www.dsh.so/artifact/dsh-plugin-upgrader/)
|
|
7
|
+
[](https://dsh.market/)
|
|
8
|
+
[](https://deepseekharnessplugins.com/plugins/zuwenyin/dsh-plugin-upgrader)
|
|
5
9
|
|
|
6
10
|
> 给 DeepSeek Harness(DSH)的「插件」页补上**一键升级**和**版本历史 / 回滚**。
|
|
7
11
|
|
|
@@ -20,6 +24,8 @@ DSH 自带的插件页能装、能卸、能看已安装列表,但缺两件事
|
|
|
20
24
|
每步都有已用时计时和不确定态进度条,卡住时可以一键取消。
|
|
21
25
|
- **多选升级** —— 勾选任意几个插件,只升级选中的这些。
|
|
22
26
|
- **版本历史 / 回滚** —— 展开任意插件即可看到已发布的全部版本(含发布时间与引擎要求),点一下切到那个版本。
|
|
27
|
+
每个版本都会先跟你**当前跑的 DSH** 比对一次:装不了的版本会被标出来,切换按钮也是禁用的
|
|
28
|
+
(宿主的兼容性闸门是在 bundle 已经卸下之后才拒绝的,不能让它走到那一步)。
|
|
23
29
|
- **一键跳 GitHub** —— 每行的 `GH` 按钮打开插件仓库主页;点版本号直接进该版本的 GitHub 发布页。
|
|
24
30
|
- **镜像落后会标出来** —— 配置的镜像还没同步、但上游已经发布的版本会单独标一行,
|
|
25
31
|
而不是假装已经是最新版。
|
|
@@ -258,6 +264,29 @@ atomic-write: timed out waiting for the writer lock at …/package.json.lock
|
|
|
258
264
|
`dshOperations`(install / start / uninstall / rollback)同理,没有逐版本的实测记录就留空,
|
|
259
265
|
不写 `passed`。
|
|
260
266
|
|
|
267
|
+
### 每个版本对不对得上现在的 DSH
|
|
268
|
+
|
|
269
|
+
DSH 在装插件时会检查插件声明的 DSH 要求,不满足就**拒绝安装**。这个检查发生在版本已经被
|
|
270
|
+
摘掉之后(老路径是先卸再装),所以「点了切换,结果插件没了」是真实发生过的事。因此页面自己
|
|
271
|
+
先算一遍,规则和宿主完全一致:
|
|
272
|
+
|
|
273
|
+
- 插件声明了 `engines.dsh` 时,**只看它**,`peerDependencies` 被忽略;
|
|
274
|
+
- 否则,`peerDependencies` 里每一个 `@deepseek-ai/dsh`、`@deepseek-ai/dsh-*` 都要满足,
|
|
275
|
+
`workspace:^` / `~` / `*` 视为「就是当前这个运行时」;
|
|
276
|
+
- 预发布版本参与比较(`includePrerelease`),与宿主的闸门相同。
|
|
277
|
+
|
|
278
|
+
两种方向都会标出来:DSH 升级之后想**降级**插件,历史里那些不再接受当前运行时的版本会
|
|
279
|
+
被标成「不支持当前 DSH」,切换按钮禁用;DSH 还停在旧版本时想**升级**,如果 `latest`
|
|
280
|
+
不接受当前运行时,它就不再算「可升级」,行上会写明它要的是哪个范围。
|
|
281
|
+
|
|
282
|
+
运行时版本是从**宿主自己的入口**读的,不是从 profile 读的:profile 里的
|
|
283
|
+
`@deepseek-ai/dsh-app-boot` 是指向 npx 缓存的链接,实测是 `0.1.2-rc.1`,而宿主真正报的是
|
|
284
|
+
`0.2.1-rc.5`。读不到时**什么都不标**——猜一个版本会把本来能装的版本说成不能装。
|
|
285
|
+
|
|
286
|
+
判定用的是 `src/host/version.ts` 里自己实现的区间匹配,已经在 49 个 range × 41 个版本共
|
|
287
|
+
2035 组上跟 `node-semver` 逐一对过(全部一致)。不引入 `semver` 依赖,是因为这条链路上
|
|
288
|
+
多一个运行时依赖就多一次供应链审阅,而这里需要的语义只有 `includePrerelease` 那一档。
|
|
289
|
+
|
|
261
290
|
## 项目结构
|
|
262
291
|
|
|
263
292
|
```
|
package/lib/client.js
CHANGED
|
@@ -87,6 +87,11 @@ window.__ModuleLoader__.load({
|
|
|
87
87
|
noHistoryWhy: "\u8FD9\u4E2A\u5305\u4E0D\u662F\u4ECE npm \u6CE8\u518C\u8868\u5B89\u88C5\u7684\uFF0C\u6CA1\u6709\u53D1\u5E03\u5386\u53F2\u3002",
|
|
88
88
|
historyFailed: "\u7248\u672C\u5217\u8868\u8BFB\u53D6\u5931\u8D25\uFF0C\u70B9\u300C\u68C0\u67E5\u66F4\u65B0\u300D\u53EF\u91CD\u8BD5",
|
|
89
89
|
mirrorBehind: "\u955C\u50CF\u6E90\u5C1A\u672A\u540C\u6B65\u8FD9\u4E9B\u5DF2\u53D1\u5E03\u7248\u672C\uFF0C\u88C5\u4E0D\u4E86",
|
|
90
|
+
incompatible: "\u4E0D\u652F\u6301\u5F53\u524D DSH",
|
|
91
|
+
incompatibleWhy: (range) => `\u8FD9\u4E2A\u7248\u672C\u8981\u6C42 DSH ${range}\uFF0C\u800C\u5F53\u524D\u5BBF\u4E3B\u4E0D\u6EE1\u8DB3\u3002\u88C5\u4E0A\u53BB\u4F1A\u88AB\u5BBF\u4E3B\u62D2\u7EDD\u3002`,
|
|
92
|
+
incompatibleLatest: (version, range) => `\u6700\u65B0\u7248 ${version} \u8981\u6C42 DSH ${range}\uFF0C\u5F53\u524D\u5BBF\u4E3B\u4E0D\u6EE1\u8DB3\uFF0C\u6240\u4EE5\u4E0D\u80FD\u5347\u4E0A\u53BB\u3002\u53EF\u4EE5\u5148\u628A DSH \u5347\u5230\u90A3\u4E2A\u8303\u56F4\uFF0C\u6216\u6362\u4E00\u4E2A\u517C\u5BB9\u7684\u7248\u672C\u3002`,
|
|
93
|
+
runtimeUnknown: "\u8BFB\u4E0D\u5230\u5F53\u524D DSH \u7248\u672C\uFF0C\u65E0\u6CD5\u5224\u65AD\u517C\u5BB9\u6027",
|
|
94
|
+
requiresDsh: (range) => `\u8981\u6C42 DSH ${range}`,
|
|
90
95
|
versions: (n) => `${n} \u4E2A\u7248\u672C`,
|
|
91
96
|
showingNewest: (n) => `\u5217\u51FA\u6700\u65B0 ${n} \u4E2A`,
|
|
92
97
|
progress: (done, total) => `\u6B63\u5728\u5904\u7406 ${done}/${total}`,
|
|
@@ -117,6 +122,7 @@ window.__ModuleLoader__.load({
|
|
|
117
122
|
restartWaiting: "\u6B63\u5728\u7B49\u5BBF\u4E3B\u56DE\u6765\u2026",
|
|
118
123
|
restartStillOld: "\u5BBF\u4E3B\u8FD8\u6CA1\u6362\u65B0\u8FDB\u7A0B\u3002\u786E\u8BA4\u65E7\u8FDB\u7A0B\u5DF2\u7ECF\u9000\u51FA\uFF0C\u518D\u8BD5\u4E00\u6B21\u3002",
|
|
119
124
|
restartReloadBlocked: "\u5BBF\u4E3B\u5DF2\u7ECF\u91CD\u542F\uFF0C\u4F46\u8FD9\u4E00\u9875\u7684\u63D2\u4EF6\u6E05\u5355\u8FC7\u671F\u4E86\uFF0C\u76F4\u63A5\u5237\u65B0\u4F1A\u542F\u52A8\u5931\u8D25\u3002\u8BF7\u518D\u91CD\u542F\u4E00\u6B21 DSH\u3002",
|
|
125
|
+
restartReloadRefused: "\u5BBF\u4E3B\u5DF2\u7ECF\u91CD\u542F\uFF0C\u4F46\u8FD9\u4E00\u9875\u6CA1\u6CD5\u81EA\u5DF1\u5237\u65B0\u3002\u624B\u52A8\u5237\u65B0\u4E00\u4E0B\uFF0C\u6216\u8005\u91CD\u542F DSH \u5B8C\u6210\u5347\u7EA7\u3002",
|
|
120
126
|
restartLoading: "\u6B63\u5728\u8BFB\u53D6\u5BBF\u4E3B\u4FE1\u606F\u2026",
|
|
121
127
|
restartLoadFailed: (detail) => `\u8BFB\u4E0D\u5230\u5BBF\u4E3B\u4FE1\u606F\uFF1A${detail}`,
|
|
122
128
|
reloadForAppearance: "\u4E3B\u9898\u63D2\u4EF6\u5DF2\u66F4\u65B0\uFF0C\u6B63\u5728\u91CD\u65B0\u52A0\u8F7D\u9875\u9762\u4EE5\u6062\u590D\u4E3B\u9898\u5916\u89C2\uFF08\u4E3B\u9898\u914D\u7F6E\u4FDD\u5B58\u5728\u78C1\u76D8\u4E0A\uFF0C\u4E0D\u4F1A\u4E22\u5931\uFF09\u3002",
|
|
@@ -220,6 +226,11 @@ window.__ModuleLoader__.load({
|
|
|
220
226
|
noHistoryWhy: "This package was not installed from an npm registry, so it has no published history.",
|
|
221
227
|
historyFailed: 'Could not read the version list; "Check for updates" retries it',
|
|
222
228
|
mirrorBehind: "Your mirror has not synced these published versions yet, so they cannot be installed",
|
|
229
|
+
incompatible: "Not for this DSH",
|
|
230
|
+
incompatibleWhy: (range) => `This version requires DSH ${range}, which this host does not satisfy. Installing it would be rejected.`,
|
|
231
|
+
incompatibleLatest: (version, range) => `The newest release ${version} requires DSH ${range}, which this host does not satisfy, so the upgrade is not offered. Upgrade DSH into that range, or pick a compatible version.`,
|
|
232
|
+
runtimeUnknown: "Could not read the running DSH version, so compatibility cannot be judged",
|
|
233
|
+
requiresDsh: (range) => `requires DSH ${range}`,
|
|
223
234
|
versions: (n) => `${n} versions`,
|
|
224
235
|
showingNewest: (n) => `showing newest ${n}`,
|
|
225
236
|
progress: (done, total) => `Working on ${done}/${total}`,
|
|
@@ -250,6 +261,7 @@ window.__ModuleLoader__.load({
|
|
|
250
261
|
restartWaiting: "Waiting for the host to come back\u2026",
|
|
251
262
|
restartStillOld: "The host has not been replaced yet. Check that the old process exited, then try again.",
|
|
252
263
|
restartReloadBlocked: "The host restarted, but this page's plugin list is out of date and reloading it would fail to start. Restart DSH once more.",
|
|
264
|
+
restartReloadRefused: "The host restarted, but this page could not reload itself. Reload it by hand, or restart DSH to finish the upgrade.",
|
|
253
265
|
restartLoading: "Reading host information\u2026",
|
|
254
266
|
restartLoadFailed: (detail) => `Could not read host information: ${detail}`,
|
|
255
267
|
reloadForAppearance: "The theme plugin was updated; reloading the page to restore its look (its settings are on disk and are not lost).",
|
|
@@ -421,15 +433,23 @@ window.__ModuleLoader__.load({
|
|
|
421
433
|
const started = await readJson(response);
|
|
422
434
|
return started.id;
|
|
423
435
|
}
|
|
436
|
+
var WATCH_MISS_LIMIT = 4;
|
|
424
437
|
async function watchJob(id, options) {
|
|
438
|
+
const wait = options.delay ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
439
|
+
let missed = 0;
|
|
425
440
|
for (; ; ) {
|
|
426
441
|
if (!options.isAlive()) return void 0;
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
442
|
+
const snapshot = await fetch(`${API}/job?id=${encodeURIComponent(id)}`, { credentials: "same-origin" }).then((response) => readJson(response)).catch(() => void 0);
|
|
443
|
+
if (snapshot === void 0) {
|
|
444
|
+
missed += 1;
|
|
445
|
+
if (missed >= WATCH_MISS_LIMIT) return void 0;
|
|
446
|
+
await wait(POLL_MS);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
missed = 0;
|
|
430
450
|
if (options.isAlive()) options.onSnapshot(snapshot);
|
|
431
451
|
if (snapshot.status !== "running" && snapshot.status !== "queued") return snapshot;
|
|
432
|
-
await
|
|
452
|
+
await wait(POLL_MS);
|
|
433
453
|
}
|
|
434
454
|
}
|
|
435
455
|
async function submitJob(steps, watch) {
|
|
@@ -772,6 +792,7 @@ window.__ModuleLoader__.load({
|
|
|
772
792
|
.dsh-pu-tag { font-size: 10px; line-height: 1; padding: 3px 6px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l1); color: var(--dsw-alias-label-secondary); font-weight: 500; white-space: nowrap; }
|
|
773
793
|
.dsh-pu-tag.is-new { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent); }
|
|
774
794
|
.dsh-pu-tag.is-err { border-color: var(--dsw-alias-state-error-primary); color: var(--dsw-alias-state-error-primary); }
|
|
795
|
+
.dsh-pu-tag.is-foreign { border-color: var(--dsw-alias-state-warn-primary); color: var(--dsw-alias-state-warn-primary); }
|
|
775
796
|
.dsh-pu-vers { display: flex; align-items: center; gap: 6px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; color: var(--dsw-alias-label-secondary); flex: none; }
|
|
776
797
|
.dsh-pu-vers b { color: var(--dsw-alias-label-primary); font-weight: 600; }
|
|
777
798
|
.dsh-pu-vers i { font-style: normal; color: var(--dsw-alias-label-tertiary); }
|
|
@@ -784,6 +805,7 @@ window.__ModuleLoader__.load({
|
|
|
784
805
|
.dsh-pu-hist-row { display: flex; align-items: center; gap: 10px; padding: 5px 6px; border-radius: 6px; font-size: 11px; }
|
|
785
806
|
.dsh-pu-hist-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
786
807
|
.dsh-pu-hist-row.is-current { background: var(--dsw-alias-interactive-bg-hover); box-shadow: inset 2px 0 0 var(--dsw-alias-brand-primary); }
|
|
808
|
+
.dsh-pu-hist-row.is-foreign .dsh-pu-mono, .dsh-pu-hist-row.is-foreign time { opacity: .6; }
|
|
787
809
|
.dsh-pu-hist-row .dsh-pu-mono { flex: none; min-width: 108px; color: var(--dsw-alias-label-primary); }
|
|
788
810
|
.dsh-pu-link { display: inline-flex; align-items: center; gap: 4px; border: 0; padding: 0; margin: 0; background: none; cursor: pointer; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; color: var(--dsw-alias-link); text-decoration: underline; text-decoration-color: color-mix(in srgb, var(--dsw-alias-link) 40%, transparent); text-underline-offset: 2px; }
|
|
789
811
|
.dsh-pu-link:hover { color: var(--dsw-alias-brand-primary); text-decoration-color: currentColor; }
|
|
@@ -905,9 +927,11 @@ window.__ModuleLoader__.load({
|
|
|
905
927
|
const link = resolveVersionLink(row, version);
|
|
906
928
|
const linkLabel = link !== void 0 && /\/releases\/tag\//.test(link) ? t("githubVersion") : t("githubReleases");
|
|
907
929
|
const title = link === void 0 ? void 0 : `${linkLabel} \u2192${link}`;
|
|
930
|
+
const foreign = version.compatible === false;
|
|
931
|
+
const why = foreign ? t("incompatibleWhy", version.requires ?? "") : title;
|
|
908
932
|
return h(
|
|
909
933
|
"div",
|
|
910
|
-
{ className: `dsh-pu-hist-row${version.current ? " is-current" : ""}` },
|
|
934
|
+
{ className: `dsh-pu-hist-row${version.current ? " is-current" : ""}${foreign ? " is-foreign" : ""}` },
|
|
911
935
|
link !== void 0 ? h(
|
|
912
936
|
"button",
|
|
913
937
|
{
|
|
@@ -921,6 +945,7 @@ window.__ModuleLoader__.load({
|
|
|
921
945
|
h("time", null, formatDate(version.time)),
|
|
922
946
|
version.latest ? h("span", { className: "dsh-pu-tag is-new" }, t("latest")) : null,
|
|
923
947
|
version.current ? h("span", { className: "dsh-pu-tag" }, t("current")) : null,
|
|
948
|
+
foreign ? h("span", { className: "dsh-pu-tag is-foreign", title: why }, t("incompatible")) : null,
|
|
924
949
|
h("span", { className: "dsh-pu-note" }, note),
|
|
925
950
|
link === void 0 ? null : h(
|
|
926
951
|
"button",
|
|
@@ -938,7 +963,8 @@ window.__ModuleLoader__.load({
|
|
|
938
963
|
{
|
|
939
964
|
type: "button",
|
|
940
965
|
className: "dsh-pu-btn is-tiny",
|
|
941
|
-
disabled: busy || version.current === true,
|
|
966
|
+
disabled: busy || version.current === true || foreign,
|
|
967
|
+
title: why,
|
|
942
968
|
onClick: () => onSwitch(row, version.version)
|
|
943
969
|
},
|
|
944
970
|
/*
|
|
@@ -1025,6 +1051,20 @@ window.__ModuleLoader__.load({
|
|
|
1025
1051
|
},
|
|
1026
1052
|
`${t("mirrorBehind")}: ${row.mirrorMissing.slice(0, 3).join(", ")}${row.mirrorMissing.length > 3 ? " \u2026" : ""}`
|
|
1027
1053
|
) : null,
|
|
1054
|
+
/*
|
|
1055
|
+
* The newest release refuses to run on this DSH version. The row's
|
|
1056
|
+
* upgrade button points at `latest`, so it is no longer an upgrade:
|
|
1057
|
+
* naming the requirement is the only thing that turns "nothing happens"
|
|
1058
|
+
* into something the person can act on.
|
|
1059
|
+
*/
|
|
1060
|
+
row.blocked !== void 0 ? h(
|
|
1061
|
+
"div",
|
|
1062
|
+
{
|
|
1063
|
+
className: "dsh-pu-note is-err",
|
|
1064
|
+
title: t("incompatibleLatest", row.blocked.version, row.blocked.requires)
|
|
1065
|
+
},
|
|
1066
|
+
t("incompatibleLatest", row.blocked.version, row.blocked.requires)
|
|
1067
|
+
) : null,
|
|
1028
1068
|
failure !== void 0 && retriable ? h(
|
|
1029
1069
|
"div",
|
|
1030
1070
|
{ className: "dsh-pu-note is-err", title: failure },
|
|
@@ -1139,6 +1179,9 @@ window.__ModuleLoader__.load({
|
|
|
1139
1179
|
// src/client/components/RestartDialog.ts
|
|
1140
1180
|
var PROBE_MS = 1e3;
|
|
1141
1181
|
var WAIT_LIMIT_MS = 12e4;
|
|
1182
|
+
function stalledMessage(problem, t) {
|
|
1183
|
+
return problem.trim() === "" ? t("restartStillOld") : problem;
|
|
1184
|
+
}
|
|
1142
1185
|
function RestartDialog(props) {
|
|
1143
1186
|
const { t, onClose } = props;
|
|
1144
1187
|
const h = getH();
|
|
@@ -1173,6 +1216,7 @@ window.__ModuleLoader__.load({
|
|
|
1173
1216
|
const waitForRestart = React.useCallback(() => {
|
|
1174
1217
|
const before = host?.pid;
|
|
1175
1218
|
if (before === void 0) return;
|
|
1219
|
+
setProblem("");
|
|
1176
1220
|
setPhase("waiting");
|
|
1177
1221
|
const startedAt = Date.now();
|
|
1178
1222
|
const timer = window.setInterval(() => {
|
|
@@ -1193,7 +1237,10 @@ window.__ModuleLoader__.load({
|
|
|
1193
1237
|
setPhase("stalled");
|
|
1194
1238
|
}
|
|
1195
1239
|
}).then((done) => {
|
|
1196
|
-
if (!done && !blocked)
|
|
1240
|
+
if (!done && !blocked) {
|
|
1241
|
+
setProblem(t("restartReloadRefused"));
|
|
1242
|
+
setPhase("stalled");
|
|
1243
|
+
}
|
|
1197
1244
|
});
|
|
1198
1245
|
}).catch(() => {
|
|
1199
1246
|
});
|
|
@@ -1257,7 +1304,7 @@ window.__ModuleLoader__.load({
|
|
|
1257
1304
|
),
|
|
1258
1305
|
host === void 0 ? null : h("p", { className: "dsh-pu-hint" }, t("restartPid", host.pid, host.profileName ?? "\u2014")),
|
|
1259
1306
|
phase === "waiting" ? h("p", { className: "dsh-pu-hint" }, t("restartWaiting")) : null,
|
|
1260
|
-
phase === "stalled" ? h("p", { className: "dsh-pu-hint" }, t
|
|
1307
|
+
phase === "stalled" ? h("p", { className: "dsh-pu-hint" }, stalledMessage(problem, t)) : null,
|
|
1261
1308
|
h(
|
|
1262
1309
|
"div",
|
|
1263
1310
|
{ className: "dsh-pu-src-foot" },
|
package/lib/host/install.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface StepOutcome {
|
|
|
26
26
|
detail?: string;
|
|
27
27
|
pendingBuilds?: string[];
|
|
28
28
|
warnings?: string[];
|
|
29
|
+
/** The version this step put back after destroying the one that was there. */
|
|
30
|
+
rolledBack?: string;
|
|
29
31
|
packageResult?: {
|
|
30
32
|
exitCode: number;
|
|
31
33
|
kind?: string;
|
|
@@ -35,6 +37,21 @@ export interface StepOutcome {
|
|
|
35
37
|
}
|
|
36
38
|
/** Allowed per-step actions. */
|
|
37
39
|
export declare const ACTIONS: Set<string>;
|
|
40
|
+
/**
|
|
41
|
+
* One version's DSH requirement, looked up only when a step is about to run.
|
|
42
|
+
*
|
|
43
|
+
* The lookup is injected rather than imported so this module keeps no registry
|
|
44
|
+
* client: the caller already has the packument cache and the chosen registry,
|
|
45
|
+
* and asking the network here would duplicate both.
|
|
46
|
+
*/
|
|
47
|
+
export interface InstallGuard {
|
|
48
|
+
/** The DSH build this process runs, when it could be read. */
|
|
49
|
+
runtimeVersion?: string | undefined;
|
|
50
|
+
/** The requirement `name@version` declares, or `undefined` when unknown. */
|
|
51
|
+
requirementOf: (name: string, version: string) => Promise<{
|
|
52
|
+
range: string;
|
|
53
|
+
} | undefined>;
|
|
54
|
+
}
|
|
38
55
|
/**
|
|
39
56
|
* Re-apply the enabled state a step asked for after the package was replaced.
|
|
40
57
|
*
|
|
@@ -78,9 +95,10 @@ export declare function restoreSelection(manager: PluginManager, name: string, l
|
|
|
78
95
|
* @param log - progress sink.
|
|
79
96
|
* @param announce - receives the request id the step installs under.
|
|
80
97
|
* @param registry - the registry chosen in this plugin's config, if any.
|
|
98
|
+
* @param guard - the compatibility check, when the caller can supply one.
|
|
81
99
|
* @returns the outcome record.
|
|
82
100
|
*/
|
|
83
|
-
export declare function runStep(manager: PluginManager, step: StepRecord, log: LogSink, announce?: RequestIdSink, registry?: string): Promise<StepOutcome>;
|
|
101
|
+
export declare function runStep(manager: PluginManager, step: StepRecord, log: LogSink, announce?: RequestIdSink, registry?: string, guard?: InstallGuard): Promise<StepOutcome>;
|
|
84
102
|
/**
|
|
85
103
|
* Whether a step names something this plugin is willing to install.
|
|
86
104
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/host/install.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAc,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../../src/host/install.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAc,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAIvF,iCAAiC;AACjC,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;AAE/C;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAA;AAEvD,yDAAyD;AACzD,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,EAAE,EAAE,OAAO,CAAA;IACX,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,WAAW,CAAC,EAAE,YAAY,CAAC,aAAa,CAAC,CAAA;IACzC,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,KAAK,CAAC,EAAE,SAAS,GAAG,QAAQ,GAAG,QAAQ,CAAA;IACvC,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;IACnB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,aAAa,CAAC,EAAE;QACd,QAAQ,EAAE,MAAM,CAAA;QAChB,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,QAAQ,CAAC,EAAE,OAAO,CAAA;QAClB,IAAI,CAAC,EAAE,MAAM,CAAA;KACd,CAAA;CACF;AAED,gCAAgC;AAChC,eAAO,MAAM,OAAO,aAAyC,CAAA;AA+H7D;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,8DAA8D;IAC9D,cAAc,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACnC,4EAA4E;IAC5E,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,SAAS,CAAC,CAAA;CACzF;AA8GD;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,EAC9B,MAAM,EAAE,OAAO,GAAG,SAAS,EAC3B,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAuB9B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,OAAO,GACX,OAAO,CAAC,OAAO,CAAC,CAmBlB;AA0MD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,OAAO,CAC3B,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,UAAU,EAChB,GAAG,EAAE,OAAO,EACZ,QAAQ,CAAC,EAAE,aAAa,EACxB,QAAQ,CAAC,EAAE,MAAM,EACjB,KAAK,CAAC,EAAE,YAAY,GACnB,OAAO,CAAC,WAAW,CAAC,CAuLtB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAM9D"}
|
package/lib/host/install.js
CHANGED
|
@@ -9,10 +9,20 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { explainLockTimeout } from './lock.js';
|
|
11
11
|
import { confirmDependency, dropDependency, profileDirFrom, restoreDependency } from './manifest.js';
|
|
12
|
+
import { satisfiesRange } from './version.js';
|
|
12
13
|
/** Allowed per-step actions. */
|
|
13
14
|
export const ACTIONS = new Set(['upgrade', 'switch']);
|
|
14
15
|
/** The manager's code for "a live loader fiber still owns this bundle". */
|
|
15
16
|
const IN_USE = 'bundle-in-use';
|
|
17
|
+
/**
|
|
18
|
+
* The manager's code for "this version does not admit the running DSH".
|
|
19
|
+
*
|
|
20
|
+
* Worth naming: it is the one refusal that is *certain* to repeat. Every other
|
|
21
|
+
* failure might be a flaky registry or a busy lock, but a version whose declared
|
|
22
|
+
* range rejects this runtime will be rejected again no matter how many times the
|
|
23
|
+
* replacement is retried — so retrying it by removing first only destroys more.
|
|
24
|
+
*/
|
|
25
|
+
const INCOMPATIBLE = 'incompatible-version';
|
|
16
26
|
/**
|
|
17
27
|
* Read the manager's failure code off a result or a throw.
|
|
18
28
|
*
|
|
@@ -86,6 +96,43 @@ async function readsAsEnabled(manager, name) {
|
|
|
86
96
|
return undefined;
|
|
87
97
|
}
|
|
88
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* One bundle as the manager currently reports it.
|
|
101
|
+
*
|
|
102
|
+
* "Absent" and "present but with no version" have to be told apart: the first
|
|
103
|
+
* means a rollback has to reinstall, the second only means the listing did not
|
|
104
|
+
* carry a version — re-selecting is still the right repair there.
|
|
105
|
+
*
|
|
106
|
+
* @param manager - the plugin manager.
|
|
107
|
+
* @param name - the bundle to read.
|
|
108
|
+
* @returns the entry, or `undefined` when it is not listed at all.
|
|
109
|
+
*/
|
|
110
|
+
async function bundleOf(manager, name) {
|
|
111
|
+
try {
|
|
112
|
+
const bundles = await manager.listBundles();
|
|
113
|
+
const list = Array.isArray(bundles) ? bundles : [];
|
|
114
|
+
return list.find((item) => item?.name === name);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The version a bundle currently has on disk, read before anything is removed.
|
|
122
|
+
*
|
|
123
|
+
* This is the value a failed replacement rolls back to. It has to be read up
|
|
124
|
+
* front: after `removeForReplacement` the package is gone, and the manager has
|
|
125
|
+
* nothing left to report a version for — which is precisely the moment the
|
|
126
|
+
* rollback needs to know what to put back.
|
|
127
|
+
*
|
|
128
|
+
* @param manager - the plugin manager.
|
|
129
|
+
* @param name - the bundle to read.
|
|
130
|
+
* @returns the installed version, or `undefined` when it cannot be read.
|
|
131
|
+
*/
|
|
132
|
+
async function installedVersion(manager, name) {
|
|
133
|
+
const version = (await bundleOf(manager, name))?.version;
|
|
134
|
+
return typeof version === 'string' && version.trim() !== '' ? version.trim() : undefined;
|
|
135
|
+
}
|
|
89
136
|
/**
|
|
90
137
|
* Call `setBundleEnabled`, reporting a refusal instead of throwing it.
|
|
91
138
|
*
|
|
@@ -267,6 +314,86 @@ export async function restoreSelection(manager, name, log) {
|
|
|
267
314
|
}
|
|
268
315
|
return true;
|
|
269
316
|
}
|
|
317
|
+
/**
|
|
318
|
+
* Put the previous version back after a replacement destroyed it.
|
|
319
|
+
*
|
|
320
|
+
* The in-place route only knows the install was refused after pnpm has run, and
|
|
321
|
+
* the manager's compatibility gate runs *after* pnpm rewrote the lockfile — so a
|
|
322
|
+
* version the host refuses can leave the package gone while the manifest entry
|
|
323
|
+
* is the only thing that got restored. The plugin then disappears from the page
|
|
324
|
+
* with no way back except reinstalling by hand.
|
|
325
|
+
*
|
|
326
|
+
* Installing the old version again is the repair that actually works: pnpm puts
|
|
327
|
+
* the files back, and the manager activates them exactly as it did before. It is
|
|
328
|
+
* deliberately attempted for every failure that follows a removal, not only the
|
|
329
|
+
* compatibility one — an entry pointing at a version that is not on disk is
|
|
330
|
+
* broken either way.
|
|
331
|
+
*
|
|
332
|
+
* Best effort: the step has already failed for its own reason, and a rollback
|
|
333
|
+
* that also fails must not replace that reason.
|
|
334
|
+
*
|
|
335
|
+
* @param manager - the plugin manager.
|
|
336
|
+
* @param step - the step that failed.
|
|
337
|
+
* @param previous - the version that was installed before the step ran.
|
|
338
|
+
* @param options - the install options the caller already built.
|
|
339
|
+
* @param log - progress sink.
|
|
340
|
+
* @param announce - receives the request id the rollback installs under.
|
|
341
|
+
* @returns whether the old version is back.
|
|
342
|
+
*/
|
|
343
|
+
async function rollBack(manager, step, previous, options, log, announce) {
|
|
344
|
+
const spec = `${step.name}@${previous}`;
|
|
345
|
+
const requestId = globalThis.crypto?.randomUUID?.() ?? `pu-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
346
|
+
announce?.(requestId);
|
|
347
|
+
log(`roll back: install ${spec} again (the failed replacement already removed it)`);
|
|
348
|
+
try {
|
|
349
|
+
const result = await manager.installBundle(spec, { ...options, requestId });
|
|
350
|
+
const back = result?.application === 'applied' || result?.application === 'restart-required';
|
|
351
|
+
if (result?.error !== undefined || !back) {
|
|
352
|
+
log(` ! rollback refused (${result?.error?.code ?? 'no change'}); ${step.name}@${previous} is not installed — ` +
|
|
353
|
+
'install it again from the Installed tab');
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
log(` ✓ ${step.name}@${previous} is back where it was`);
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
log(` ! rollback failed: ${describeFailure(error instanceof Error ? error.message : String(error))}; ` +
|
|
361
|
+
`${step.name}@${previous} is not installed`);
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Put the profile back the way the step found it, whatever it managed to break.
|
|
367
|
+
*
|
|
368
|
+
* Two different damages need two different repairs, and picking the wrong one
|
|
369
|
+
* makes things worse:
|
|
370
|
+
*
|
|
371
|
+
* - the package is still on disk but was deselected — re-selecting it is enough,
|
|
372
|
+
* and installing an old version over it would be a pointless pnpm run;
|
|
373
|
+
* - the package is gone — re-selecting nothing, which is what the old code did,
|
|
374
|
+
* leaves an entry in `dependencies` that nothing can load, so the plugin is
|
|
375
|
+
* simply missing from the page.
|
|
376
|
+
*
|
|
377
|
+
* @param manager - the plugin manager.
|
|
378
|
+
* @param step - the step that failed.
|
|
379
|
+
* @param previous - the version installed before the step ran, when known.
|
|
380
|
+
* @param options - the install options the caller already built.
|
|
381
|
+
* @param log - progress sink.
|
|
382
|
+
* @param announce - receives the request id a rollback installs under.
|
|
383
|
+
* @returns whether the profile was put back.
|
|
384
|
+
*/
|
|
385
|
+
async function recover(manager, step, previous, options, log, announce) {
|
|
386
|
+
const entry = await bundleOf(manager, step.name);
|
|
387
|
+
if (entry !== undefined && entry.installed !== false) {
|
|
388
|
+
await restoreSelection(manager, step.name, log);
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
if (previous === undefined) {
|
|
392
|
+
log(` ! ${step.name} is no longer installed, and its previous version was not recorded`);
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
return rollBack(manager, step, previous, options, log, announce);
|
|
396
|
+
}
|
|
270
397
|
/**
|
|
271
398
|
* Replace a version without removing the bundle first.
|
|
272
399
|
*
|
|
@@ -367,9 +494,10 @@ async function replaceWithoutRemoval(manager, step, options, log, announce) {
|
|
|
367
494
|
* @param log - progress sink.
|
|
368
495
|
* @param announce - receives the request id the step installs under.
|
|
369
496
|
* @param registry - the registry chosen in this plugin's config, if any.
|
|
497
|
+
* @param guard - the compatibility check, when the caller can supply one.
|
|
370
498
|
* @returns the outcome record.
|
|
371
499
|
*/
|
|
372
|
-
export async function runStep(manager, step, log, announce, registry) {
|
|
500
|
+
export async function runStep(manager, step, log, announce, registry, guard) {
|
|
373
501
|
const spec = `${step.name}@${step.version}`;
|
|
374
502
|
const requestId = globalThis.crypto?.randomUUID?.() ?? `pu-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
375
503
|
announce?.(requestId);
|
|
@@ -379,6 +507,29 @@ export async function runStep(manager, step, log, announce, registry) {
|
|
|
379
507
|
const base = { name: step.name, version: step.version, spec, requestId };
|
|
380
508
|
const restoreEnabled = typeof step.enabled === 'boolean' ? step.enabled : undefined;
|
|
381
509
|
log(`install ${spec}`);
|
|
510
|
+
/*
|
|
511
|
+
* Ask the version itself before touching anything.
|
|
512
|
+
*
|
|
513
|
+
* The host's own gate is the final word and it runs inside pnpm's wake-up, so
|
|
514
|
+
* without this check a refusal arrives after the manager has already deselected
|
|
515
|
+
* — or removed — the bundle. Reading the published requirement first turns
|
|
516
|
+
* "your plugin is gone" into "this version was never going to work", which is
|
|
517
|
+
* the difference between a repair and a loss.
|
|
518
|
+
*/
|
|
519
|
+
const requirement = guard === undefined ? undefined : await guard.requirementOf(step.name, step.version);
|
|
520
|
+
if (requirement !== undefined && guard?.runtimeVersion !== undefined) {
|
|
521
|
+
if (!satisfiesRange(guard.runtimeVersion, requirement.range)) {
|
|
522
|
+
log(` ! ${spec} declares ${requirement.range}, which excludes DSH ${guard.runtimeVersion}; nothing was changed`);
|
|
523
|
+
return {
|
|
524
|
+
...base,
|
|
525
|
+
ok: false,
|
|
526
|
+
application: 'failed',
|
|
527
|
+
stage: 'install',
|
|
528
|
+
message: INCOMPATIBLE,
|
|
529
|
+
detail: requirement.range,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
}
|
|
382
533
|
const options = {
|
|
383
534
|
saveExact: true,
|
|
384
535
|
...(restoreEnabled === undefined ? {} : { enabled: restoreEnabled }),
|
|
@@ -392,6 +543,12 @@ export async function runStep(manager, step, log, announce, registry) {
|
|
|
392
543
|
...(typeof registry === 'string' && registry.trim() !== '' ? { registry: registry.trim() } : {}),
|
|
393
544
|
};
|
|
394
545
|
let result;
|
|
546
|
+
/*
|
|
547
|
+
* Read the version on disk *before* the manager can take it away. This is the
|
|
548
|
+
* value a failed replacement rolls back to, and once `removeForReplacement`
|
|
549
|
+
* has run there is nothing left to ask.
|
|
550
|
+
*/
|
|
551
|
+
const previous = await installedVersion(manager, step.name);
|
|
395
552
|
try {
|
|
396
553
|
result = await manager.installBundle(spec, { ...options, requestId });
|
|
397
554
|
}
|
|
@@ -462,12 +619,12 @@ export async function runStep(manager, step, log, announce, registry) {
|
|
|
462
619
|
}
|
|
463
620
|
catch (error) {
|
|
464
621
|
/*
|
|
465
|
-
* The removal above
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
622
|
+
* The removal above happened for real, so a throw here can leave the
|
|
623
|
+
* plugin gone. `recover` tells the two damages apart: still-on-disk means
|
|
624
|
+
* re-select it, gone means install the previous version again. Confusing
|
|
625
|
+
* them is what made "upgrade failed" also mean "plugin disappeared".
|
|
469
626
|
*/
|
|
470
|
-
await
|
|
627
|
+
const back = await recover(manager, step, previous, options, log, announce);
|
|
471
628
|
return {
|
|
472
629
|
...base,
|
|
473
630
|
requestId: retryId,
|
|
@@ -475,6 +632,7 @@ export async function runStep(manager, step, log, announce, registry) {
|
|
|
475
632
|
stage: 'install',
|
|
476
633
|
application: 'failed',
|
|
477
634
|
message: `install after removal failed: ${describeFailure(error instanceof Error ? error.message : String(error))}`,
|
|
635
|
+
...(back ? { rolledBack: previous } : {}),
|
|
478
636
|
};
|
|
479
637
|
}
|
|
480
638
|
}
|
|
@@ -483,8 +641,7 @@ export async function runStep(manager, step, log, announce, registry) {
|
|
|
483
641
|
* removal already done. Same hazard, same repair; the reported reason stays
|
|
484
642
|
* whatever the manager said.
|
|
485
643
|
*/
|
|
486
|
-
|
|
487
|
-
await restoreSelection(manager, step.name, log);
|
|
644
|
+
const restored = result?.error === undefined ? false : await recover(manager, step, previous, options, log, announce);
|
|
488
645
|
// Only a change that landed has an enabled state to restore: a failed install
|
|
489
646
|
// left nothing behind, and asking to toggle it would report a second, louder
|
|
490
647
|
// error (`operation-error`) for a package that was never added.
|
|
@@ -494,6 +651,7 @@ export async function runStep(manager, step, log, announce, registry) {
|
|
|
494
651
|
...base,
|
|
495
652
|
...(enabled === undefined ? {} : { enabled }),
|
|
496
653
|
ok: installed,
|
|
654
|
+
...(restored ? { rolledBack: previous } : {}),
|
|
497
655
|
application: result?.application,
|
|
498
656
|
changed: result?.changed === true,
|
|
499
657
|
stage: result?.stage,
|