dsh-plugin-update 0.1.1 → 0.1.2
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/README.md +255 -290
- package/derive-client-values.mjs +5 -2
- package/dist/client.js +1 -1
- package/dist/commands.js +13 -2
- package/dist/config.js +1 -1
- package/dist/gate.js +1 -1
- package/dist/host.js +20 -6
- package/dist/ports.js +2 -2
- package/dist/reader.js +7 -3
- package/dist/service.js +12 -4
- package/dist/store.js +134 -3
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -1,290 +1,255 @@
|
|
|
1
|
-
# dsh-plugin-update
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
`
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
|
116
|
-
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
confirmationTtlMs: 20 * 60_000,
|
|
257
|
-
panelPollMs: 2000,
|
|
258
|
-
}
|
|
259
|
-
)
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
客户端轮询间隔同样走 `assertPollInterval` 校验,小于 250 毫秒直接抛错。
|
|
263
|
-
另有两处固定不变、不要当成可调项写进文档:查新版 2 秒内重复点击复用上次结果、安装执行终止宽限 3 秒。
|
|
264
|
-
|
|
265
|
-
凭证过期(`check-expired`)是正常错误码,不是程序缺陷:用户拿着过期的检查编号点安装一定失败,面板此时重新调一次查新版、拿新凭证再提交即可。
|
|
266
|
-
|
|
267
|
-
## 12. 排错
|
|
268
|
-
|
|
269
|
-
按从常见到少见的顺序查,一次只动一处,动完重查一次状态。
|
|
270
|
-
|
|
271
|
-
1. `pluginId` 报错说必填或含路径分隔符:换成自己插件的标识,非空且不含 `/` `\`。
|
|
272
|
-
2. 电话名串台(两家插件收到对方的电话):检查两家 `prefix` 是否相同,相同即改成不同的前缀;电话名永远从 `update.phoneNames` 读到,不要自己拼字符串。
|
|
273
|
-
3. 面板轮询报错说小于 250 毫秒:把 `panelPollMs` 调到 250 或更大。
|
|
274
|
-
4. 查新版总超时:先调大 `checkTimeoutMs`,再检查官方源地址是否写错、网络是否通官方源。
|
|
275
|
-
5. 点安装报 `check-expired`:凭证过期了,重新查一次新版再点安装;不要重试旧编号。
|
|
276
|
-
6. 点安装报 `update-busy`:同一使用范围同时只装一个,等当前任务结束再点;面板轮询看到任务离开 `installing`/`verifying` 即为空闲。
|
|
277
|
-
7. 手工命令为空:对照第 8 节前两行(源码安装或认不出使用范围),先修范围再要命令。
|
|
278
|
-
8. 装完版本号没变:先看是否 `pending-restart`(见第 10 节),是则重启宿主;不是则按第 8 节的表格查原因。
|
|
279
|
-
9. 桌面宿主自动装失败但命令端能装:检查桌面当前激活的使用范围是不是插件所在的那一个,对不上时自动装一定诚实失败,复制第 9 节命令手工执行。
|
|
280
|
-
10. 还定位不到:打开调试日志,按插件标识过滤 `host.call`、`host.call.fail`、`update.install.exec` 三个事件,看 `pluginId` 与 `route`、`exitCode` 字段;日志里不记命令与路径原文,只看枚举与数字即可定位。
|
|
281
|
-
|
|
282
|
-
## 13. 新人三步自查(默认路径跑通清单)
|
|
283
|
-
|
|
284
|
-
新人照第 3 节例子把名字换成自己的之后,按下面三条逐项打勾,全勾即默认路径跑通:
|
|
285
|
-
|
|
286
|
-
1. 第 2 步调通:宿主启动不报错,`update.phoneNames` 读到自家前缀的三电话名(例如 `notes.updateStatus`),查状态返回六字段快照。
|
|
287
|
-
2. 第 3 步调通:面板按间隔轮询到快照,能展示第 8 节的原因文案;有新版时安装按钮可用,无新版与待重启时按钮状态正确。
|
|
288
|
-
3. 待重启与兜底可见:模拟一次 `pending-restart` 看到第 10 节横幅,模拟一次自动装失败看到第 9 节手工命令可复制执行。
|
|
289
|
-
|
|
290
|
-
本节三条全绿即达到本票验收“新人照文档三步跑通默认路径”。
|
|
1
|
+
# dsh-plugin-update
|
|
2
|
+
|
|
3
|
+
给 DSH 插件加「检查更新 / 安装更新」能力的 npm 包。宿主侧一段接线,面板侧构建期派生取值,装不上时给用户一条可复制的手工命令。
|
|
4
|
+
|
|
5
|
+
要求 Node 22 或更高,零运行时依赖。当前版本 `0.1.2`。
|
|
6
|
+
|
|
7
|
+
装上它你会拿到三样东西:
|
|
8
|
+
|
|
9
|
+
- **三个电话**:查状态(只读本地)、查新版(用户点了才联网一次)、装更新(拿凭证提交)。电话指宿主对外提供的方法。
|
|
10
|
+
- **一套落盘**:任务状态、安装锁、回滚凭据,按「插件标识 + 使用范围」隔离,多插件互不干扰。
|
|
11
|
+
- **面板要的派生取值**:电话名与轮询间隔,构建期从本包生成,面板里不写死。
|
|
12
|
+
|
|
13
|
+
## 1. 安装
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npm install dsh-plugin-update
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
包还没公开发布时先用本地路径代替(例如 `npm install ../dsh-plugin-update`)。
|
|
20
|
+
|
|
21
|
+
## 2. 三步接入
|
|
22
|
+
|
|
23
|
+
下面假设你的插件标识是 `my-notes-plugin`、电话名前缀是 `notes`、要查更新的包也是 `my-notes-plugin`。
|
|
24
|
+
三处名字都换成你自己的;`prefix` 不传即 `wf`(历史取值),新插件务必传自己的前缀,多插件靠它隔离。
|
|
25
|
+
|
|
26
|
+
### 第 1 步:装包
|
|
27
|
+
|
|
28
|
+
见第 1 节。
|
|
29
|
+
|
|
30
|
+
### 第 2 步:宿主侧接线
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
import { createHostUpdate } from 'dsh-plugin-update'
|
|
34
|
+
|
|
35
|
+
const update = createHostUpdate(
|
|
36
|
+
{ ctx, logCtx },
|
|
37
|
+
{
|
|
38
|
+
pluginId: 'my-notes-plugin',
|
|
39
|
+
prefix: 'notes',
|
|
40
|
+
targetPackageName: 'my-notes-plugin',
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
// 把处理器按键注册进自己的电话表;电话名从 update.phoneNames 读,不要自己拼字符串。
|
|
44
|
+
for (const [name, handler] of Object.entries(update.handlers)) {
|
|
45
|
+
registry.set(name, handler)
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
这一步得到三个电话名:`notes.updateStatus`、`notes.updateCheck`、`notes.updateInstall`。
|
|
50
|
+
|
|
51
|
+
`pluginId` 必填(非空字符串,不含路径分隔符)。单例复用键强制含插件标识,多插件不串内存状态与锁。
|
|
52
|
+
除 `pluginId` 之外的配置都可选并带默认值,不传即走默认(见第 3 节)。
|
|
53
|
+
|
|
54
|
+
### 第 3 步:面板侧接线(构建期派生)
|
|
55
|
+
|
|
56
|
+
目标只有一句:**面板里不要写死电话名与轮询间隔**。构建时用包内工具生成一个小文件:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
node node_modules/dsh-plugin-update/derive-client-values.mjs --prefix notes --out scripts/generated/updateClient.derived.js
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
生成的常量直接在面板里用:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
// UPD_STATUS / UPD_CHECK / UPD_INSTALL / UPD_POLL 由上面那条命令生成
|
|
66
|
+
host.call(UPD_STATUS, {}) // 查状态
|
|
67
|
+
host.call(UPD_CHECK, {}) // 查新版
|
|
68
|
+
host.call(UPD_INSTALL, { checkId, requestId }) // 装更新
|
|
69
|
+
setInterval(readStatus, UPD_POLL) // 轮询
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
派生工具的自定义项:
|
|
73
|
+
|
|
74
|
+
- `--prefix` 必填,必须与宿主侧 `createHostUpdate` 传的 `prefix` 一致,否则面板调的电话名与宿主注册的对不上。
|
|
75
|
+
- `--out` 必填,故意不给默认值:默认值会覆盖别人的文件。
|
|
76
|
+
- `--dry-run` 只打印生成内容、不写文件。
|
|
77
|
+
- 工具用 `esbuild` 打包一次(构建期使用,不引入运行期依赖);找不到 esbuild 时打印安装提示,不静默失败。
|
|
78
|
+
|
|
79
|
+
以后换前缀或升级本包,重新跑一次这条命令即可。
|
|
80
|
+
|
|
81
|
+
### 升级本包(已经接入过的项目)
|
|
82
|
+
|
|
83
|
+
宿主种类与安装出口都由本包自己探测和选择,**升级依赖即可,宿主侧与面板侧都不用改代码**:
|
|
84
|
+
|
|
85
|
+
1. 依赖版本提到 `^0.1.2`。
|
|
86
|
+
2. 重新装 / 发一版你自己的插件,让新依赖进当前使用范围(运行时用的是 `node_modules` 里那份)。
|
|
87
|
+
3. 如果你自己接过一版宿主安装出口,把它删掉——它会挡在本包的路由前面。
|
|
88
|
+
|
|
89
|
+
两处例外必须动代码(它们把自动探测挡住了):显式传了 `readerOverrides.environmentKind`;显式传了与真实使用范围不符的 `profileDir` / `profileName`。
|
|
90
|
+
|
|
91
|
+
`targetPackageName` 建议就是你自己的包名:使用范围目录按「装好的包住在 `<范围>/node_modules/<目标包名>`」反推,对不上时要自己传 `profileDir`。
|
|
92
|
+
|
|
93
|
+
## 3. 配置
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
const update = createHostUpdate(
|
|
97
|
+
{ ctx, logCtx },
|
|
98
|
+
{
|
|
99
|
+
pluginId: 'my-notes-plugin',
|
|
100
|
+
prefix: 'notes',
|
|
101
|
+
targetPackageName: 'my-notes-plugin',
|
|
102
|
+
confirmationTtlMs: 20 * 60_000, // 只调这两项,其余走默认
|
|
103
|
+
panelPollMs: 2000,
|
|
104
|
+
}
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
| 配置键 | 默认 | 说明 |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `pluginId` | 必填,无默认 | 非空字符串,不含路径分隔符 |
|
|
111
|
+
| `prefix` | `wf` | 电话名前缀;电话名 = 前缀 + 点 + 动作名 |
|
|
112
|
+
| `targetPackageName` | `dsh-mattpocock-skills-deck` | 要检查更新的那个包是谁 |
|
|
113
|
+
| `registryUrl` | `https://registry.npmjs.org/` | 官方源 |
|
|
114
|
+
| `homeDir` | 现推导 | 环境变量 `DSH_HOME` 优先,否则家目录下 `.dsh` |
|
|
115
|
+
| `checkTimeoutMs` | `10000` | 联网超时,须为有限大于 0 的数 |
|
|
116
|
+
| `confirmationTtlMs` | `600000` | 凭证有效期,须为有限大于 0 的数 |
|
|
117
|
+
| `installTimeoutMs` | `900000` | 安装时限,须为有限大于 0 的数 |
|
|
118
|
+
| `panelPollMs` | `1000` | 面板轮询间隔,不得小于 250 毫秒 |
|
|
119
|
+
|
|
120
|
+
配置只经函数入参注入,不读配置文件。越界直接抛错,不静默取整。
|
|
121
|
+
|
|
122
|
+
落盘目录按标识派生:家目录下 `updates` 加插件标识加使用范围短指纹,三个文件名是 `state.json`、`install.lock`、`before.json`。读走双读(先新后旧),写只写新。
|
|
123
|
+
标识取历史值 `dsh-mattpocock-skills-deck` 时,路径与旧版一字不差(升级不丢状态)。
|
|
124
|
+
|
|
125
|
+
## 4. 安装是怎么执行的
|
|
126
|
+
|
|
127
|
+
你不需要按宿主分支:包探测当前宿主,选它自己的安装出口。
|
|
128
|
+
|
|
129
|
+
| 宿主 | 出口 | 说明 |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| 第三方 Desktop(发布 `desktopProfiles` / `desktopPnpm` 的那一支) | 桌面服务 `desktopPnpm.runPlugin` | 装进「当前激活的使用范围」;激活范围与本插件所在范围对不上时宁可不装 |
|
|
132
|
+
| 官方桌面版(宿主进程内的插件管理器) | `pluginManager.installBundle` | 只收 `包名@精确版本`,不接受任何开关 |
|
|
133
|
+
| 普通 DSH 宿主 | 自己起 `dsh plugin --profile <名> add …` | 参数数组直传;不经 shell、不用 `PATH` 上的命令名、不按系统分支 |
|
|
134
|
+
|
|
135
|
+
三种出口都只装**精确版本**。第三方 Desktop 与普通宿主把官方源写进参数(`--registry=`),官方桌面版走管理器的 `registry` 选项。
|
|
136
|
+
官方源不可达时诚实失败,不换源。装不上时宿主原话(截断到 300 字、去掉绝对路径)会写进任务说明 `job.message`,形如 `install-failed: <原话>`。
|
|
137
|
+
|
|
138
|
+
时间口径:查新版 2 秒内重复点击复用上次结果;安装按 `installTimeoutMs` 计时,到点终止(官方桌面版先请求宿主取消,来不及就等它收尾再定成败);终止宽限 3 秒。
|
|
139
|
+
|
|
140
|
+
要显式覆盖出口(例如自己把宿主管理器实例交进来):`createHostUpdate({ ctx, pluginManager })`,不传就从 `ctx` 现取。
|
|
141
|
+
|
|
142
|
+
## 5. 用户会看到什么
|
|
143
|
+
|
|
144
|
+
### 5.1 快照六字段
|
|
145
|
+
|
|
146
|
+
查状态与查新版都返回同一份快照,恰好六个字段:
|
|
147
|
+
|
|
148
|
+
| 字段 | 含义 |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `installedVersion` | 磁盘上装着的版本 |
|
|
151
|
+
| `runningVersion` | 正在跑的版本 |
|
|
152
|
+
| `latestVersion` | 最近一次查新版带回来的版本(没查过为 null) |
|
|
153
|
+
| `canInstall` | 现在能不能装 |
|
|
154
|
+
| `blockedReason` | 装不了的原因(能装为 null) |
|
|
155
|
+
| `job` | 当前任务(只读半程恒为 null) |
|
|
156
|
+
|
|
157
|
+
### 5.2 八种装不了的原因
|
|
158
|
+
|
|
159
|
+
面板拿到非空 `blockedReason` 时,直接展示下表「用户该做什么」那一列的一句话,不要只展示英文原因本身。
|
|
160
|
+
|
|
161
|
+
| 原因 | 中文含义 | 用户该做什么 |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `unknown-profile` | 使用范围认不出(名字非法或目录不存在) | 检查使用范围名是否含特殊字符、目录是否还在;这种情形不给手工命令 |
|
|
164
|
+
| `source-install` | 当前是从源码装的,不是按版本号装的 | 这种情形不给手工命令;想走更新先按版本号重装一次 |
|
|
165
|
+
| `invalid-installation` | 已装的包不完整(名字对不上、版本非法、入口文件缺失) | 重装当前版本,修好已装目录再查更新 |
|
|
166
|
+
| `installation-changed` | 安装位置在使用中途变了(换了目录或换了包) | 重新打开宿主再查一次;还出现就重装 |
|
|
167
|
+
| `pending-restart` | 新版已装到磁盘,正在跑的还是旧版 | 重启宿主,让新版跑起来;这是正常终态,不是失败 |
|
|
168
|
+
| `registry-conflict` | 本地声明的版本与磁盘实际版本互相矛盾 | 打开使用范围的清单文件,把目标包名那一行改成版本号再试 |
|
|
169
|
+
| `incompatible-node` | 新版要求的 Node 与当前运行的对不上 | 先升级 Node 到 22 或更高,再查更新 |
|
|
170
|
+
| `recovery-required` | 上次安装被打断,留下一个半截任务 | 重新点一次安装;一直出现就按第 6 节排错 |
|
|
171
|
+
|
|
172
|
+
### 5.3 手工兜底命令
|
|
173
|
+
|
|
174
|
+
每次查状态与查新版都会顺带回一条手工命令(字段名 `manual`),能给则给、不能给则为空。
|
|
175
|
+
为空是正常的两种情形:源码安装、认不出使用范围(上表前两行),此时只展示原因。
|
|
176
|
+
|
|
177
|
+
```sh
|
|
178
|
+
dsh plugin --profile my-web add --save-exact my-notes-plugin@1.2.3 --registry=https://registry.npmjs.org/
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
使用范围名含空格或特殊字符时命令里会自动加引号,复制整行执行即可;网络受限时用户可以自行去掉 `--registry=` 走本地源。
|
|
182
|
+
|
|
183
|
+
面板每次拿到回包都刷新展示这条命令,不要缓存旧的。
|
|
184
|
+
|
|
185
|
+
### 5.4 ⚠️ 待重启提示(必须做)
|
|
186
|
+
|
|
187
|
+
`pending-restart` 不是失败,是「磁盘已是新版、正在跑的还是旧版」。面板必须满足三条:
|
|
188
|
+
|
|
189
|
+
1. 用显眼样式单独展示(例如顶部横幅加 ⚠️),不要只藏在日志或悬停提示里。
|
|
190
|
+
2. 文案说清两件事:新版号是多少、重启后才生效。例如:`⚠️ 新版 1.2.3 已装好,正在跑的还是 1.2.2,重启宿主后生效。`
|
|
191
|
+
3. 待重启期间不再提供安装按钮(快照里 `canInstall` 已为假),只给重启指引与手工命令入口。
|
|
192
|
+
|
|
193
|
+
判断只看快照:`blockedReason === 'pending-restart'` 即展示;`installedVersion !== runningVersion` 只作辅助校验。
|
|
194
|
+
|
|
195
|
+
### 5.5 轮询与凭证
|
|
196
|
+
|
|
197
|
+
- 面板按 `panelPollMs` 轮询查状态;小于 250 毫秒直接抛错。
|
|
198
|
+
- 装更新要带查新版的凭证(`checkId`)与本次请求编号(`requestId`)。凭证有过期时间,过期后重新查一次新版再提交。
|
|
199
|
+
- 同一个 `requestId` 重复提交直接返回旧结果,不重装;同一使用范围同时只装一个,撞上会报 `update-busy`。
|
|
200
|
+
|
|
201
|
+
## 6. 排错
|
|
202
|
+
|
|
203
|
+
按从常见到少见的顺序查,一次只动一处,动完重查一次状态。
|
|
204
|
+
|
|
205
|
+
1. `pluginId` 报必填或含路径分隔符:换成自己插件的标识,非空且不含 `/` `\`。
|
|
206
|
+
2. 电话名串台(两家插件收到对方的电话):检查两家 `prefix` 是否相同,相同即改成不同的前缀;电话名永远从 `update.phoneNames` 读。
|
|
207
|
+
3. 面板轮询报错说小于 250 毫秒:把 `panelPollMs` 调到 250 或更大。
|
|
208
|
+
4. 查新版总超时:先调大 `checkTimeoutMs`,再检查源地址是否写错、网络是否通。
|
|
209
|
+
5. 点安装报 `check-expired`:凭证过期,重新查一次新版再点安装,不要重试旧编号。
|
|
210
|
+
6. 点安装报 `update-busy`:同一使用范围同时只装一个,等当前任务离开 `installing`/`verifying` 再点。
|
|
211
|
+
7. 手工命令为空:对照第 5.2 节前两行(源码安装或认不出使用范围),先修好再要命令。
|
|
212
|
+
8. 装完版本号没变:先看是否 `pending-restart`(第 5.4 节),是则重启宿主;不是则按第 5.2 节的表查原因。
|
|
213
|
+
9. 第三方 Desktop 自动装失败但命令能装:检查桌面当前激活的使用范围是不是插件所在的那一个;对不上时自动装一定诚实失败,复制第 5.3 节的命令手工执行。
|
|
214
|
+
10. 官方桌面版(使用范围名 `desktop`)装不上:看任务里的 `message`,形如 `install-failed: <宿主原话>`,里面带着宿主的错误码(例如 `operation-error`)。`message` 可能带详情,**匹配请匹配前缀错误码,不要整串相等**;日志里这条路的路由是 `desktop-manager`。
|
|
215
|
+
11. 还定位不到:打开调试日志,按插件标识过滤 `host.call`、`host.call.fail`、`update.install.exec` 三个事件,看 `pluginId` 与 `route`、`exitCode` 字段;日志里不记命令与路径原文。
|
|
216
|
+
|
|
217
|
+
## 7. 接入自检清单
|
|
218
|
+
|
|
219
|
+
照第 2 节把名字换成自己的之后,逐条打勾:
|
|
220
|
+
|
|
221
|
+
1. 宿主启动不报错,`update.phoneNames` 读到自家前缀的三个电话名,查状态返回六字段快照。
|
|
222
|
+
2. 面板按间隔轮询到快照,能展示第 5.2 节的原因文案;有新版时安装按钮可用,无新版与待重启时按钮状态正确。
|
|
223
|
+
3. 模拟一次 `pending-restart` 能看到第 5.4 节的横幅,模拟一次自动装失败能看到第 5.3 节的命令可复制执行。
|
|
224
|
+
4. 第二家同机隔离:两家各传自己的插件标识与电话名前缀,电话名、落盘目录、锁文件逐个不同,各拿各的锁互不阻塞。
|
|
225
|
+
|
|
226
|
+
## 8. 包还导出什么
|
|
227
|
+
|
|
228
|
+
**客户端入口**(`dist/client.js`,构建期打包用):
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
buildClientPhoneNames('notes') // 电话名拼法,与宿主侧同一套(前缀 + 点 + 动作名)
|
|
232
|
+
CLIENT_POLL.defaultMs // 面板轮询默认值(1000)
|
|
233
|
+
CLIENT_POLL.minMs // 轮询下限(250,低于它要报错而不是静默取整)
|
|
234
|
+
manualCommand({ ... }) // 手工兜底命令的形状,与宿主侧同一套政策
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**事件清单检查器**(纯函数,不新增日志事件):
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
import { parseEventListManifest, checkEventFields, checkEventCounts } from 'dsh-plugin-update'
|
|
241
|
+
|
|
242
|
+
const manifest = parseEventListManifest(myEventList) // myEventList 是对象,先自己读盘
|
|
243
|
+
checkEventFields(manifest, 'host.call', ['method', 'pluginId']) // 字段白名单
|
|
244
|
+
checkEventCounts(manifest) // 计数与清单自报的 counts 对齐
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
事件清单的每条要写四样:事件名、级别(`error`/`warn`/`info`/`debug`)、允许字段(之外的键一律不记)、脱敏引用(`codes` 是截断或散列代号,`rules` 是具名正则名——都只记引用名,不记原文)。
|
|
248
|
+
`kind` 分三类只为计数检查服务:`resident` 常驻、`ondemand` 按需、`selfmon` 自监控。空模板见包内的 `event-list.template.json`。
|
|
249
|
+
清单要以**对象**传入:字符串或数组会被拦下并给出中文说明,不静默修补。
|
|
250
|
+
|
|
251
|
+
## 9. 兼容与稳定性
|
|
252
|
+
|
|
253
|
+
这些形状稳定,可以放心依赖:三个电话名与入参回参、快照六字段、任务公开形状、配置只经函数入参注入、安装配方五键、日志事件字段基线(三个事件各带必填 `pluginId`)、历史落盘路径。
|
|
254
|
+
|
|
255
|
+
向后兼容的扩展:新增可选配置键、新增宿主种类与路由取值。调用方不认新取值时按普通宿主处理即可,不会因此报错。
|
package/derive-client-values.mjs
CHANGED
|
@@ -110,8 +110,11 @@ if (!built.outputFiles || built.outputFiles.length !== 1) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
let body = Buffer.from(built.outputFiles[0].contents).toString('utf8').replace(/\r\n/g, '\n')
|
|
113
|
-
//
|
|
114
|
-
|
|
113
|
+
// 注释里的路径归一:打包器写出的是「相对当前目录」的路径,换个目录重跑就会变成 ../ 开头,
|
|
114
|
+
// 消费方重新派生一次就得到一份「只是注释不同」的 diff。这里把前导的 ../ 一律剥掉,
|
|
115
|
+
// 路径注释固定成仓内形态,保证重跑零差异。
|
|
116
|
+
// 只动 ../ 前缀、不动别的:本包零运行期依赖,产物里的注释只会是本包自己的文件。
|
|
117
|
+
body = body.replace(/^\/\/ (?:\.\.\/)+/gm, '// ')
|
|
115
118
|
|
|
116
119
|
// 打包器收尾会写一个模块级的 `export { ... };` 块。那个块拼进插件主文件闭包是语法错误
|
|
117
120
|
// (闭包里只能是声明),所以拆掉它;被改名导出的符号在下面补回本名。
|
package/dist/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// AUTO-GENERATED by node
|
|
1
|
+
// AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/client.ts
|
|
2
2
|
import { buildPhoneNames, DEFAULT_PANEL_POLL_MS, MIN_PANEL_POLL_MS } from "./config.js";
|
|
3
3
|
import { buildPhoneName, buildPhoneNames as buildPhoneNames2 } from "./config.js";
|
|
4
4
|
import { manualCommand as manualCommand2 } from "./commands.js";
|
package/dist/commands.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// AUTO-GENERATED by node
|
|
1
|
+
// AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/commands.ts
|
|
2
2
|
const PACKAGE_NAME = "dsh-mattpocock-skills-deck";
|
|
3
3
|
const NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
4
|
+
const MANAGER_TARGET_RE = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
|
|
4
5
|
const INSTALL_TIMEOUT_MS = 15 * 6e4;
|
|
5
6
|
function validVersion(v) {
|
|
6
7
|
return typeof v === "string" && /^\d+\.\d+\.\d+$/.test(v);
|
|
@@ -39,11 +40,21 @@ function installRecipe(input) {
|
|
|
39
40
|
const version = input?.version;
|
|
40
41
|
if (!name || !validVersion(version)) return null;
|
|
41
42
|
const kind = input?.environmentKind;
|
|
42
|
-
if (kind !== "desktop" && kind !== "cli") return null;
|
|
43
|
+
if (kind !== "desktop" && kind !== "desktop-manager" && kind !== "cli") return null;
|
|
43
44
|
const targetName = input?.targetPackageName ?? PACKAGE_NAME;
|
|
44
45
|
const registry = input?.registryUrl ?? NPM_REGISTRY;
|
|
45
46
|
const timeoutMs = input?.timeoutMs ?? INSTALL_TIMEOUT_MS;
|
|
46
47
|
if (!targetName || !registry) return null;
|
|
48
|
+
if (kind === "desktop-manager") {
|
|
49
|
+
if (!MANAGER_TARGET_RE.test(targetName)) return null;
|
|
50
|
+
return {
|
|
51
|
+
route: "desktop-manager",
|
|
52
|
+
profileName: name,
|
|
53
|
+
version,
|
|
54
|
+
pluginArgs: ["add", `${targetName}@${version}`],
|
|
55
|
+
timeoutMs
|
|
56
|
+
};
|
|
57
|
+
}
|
|
47
58
|
return {
|
|
48
59
|
route: kind === "desktop" ? "desktop-service" : "cli-process",
|
|
49
60
|
profileName: name,
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// AUTO-GENERATED by node
|
|
1
|
+
// AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/config.ts
|
|
2
2
|
const DEFAULT_PREFIX = "wf";
|
|
3
3
|
const PHONE_ACTIONS = ["updateStatus", "updateCheck", "updateInstall"];
|
|
4
4
|
const DEFAULT_TARGET_PACKAGE = "dsh-mattpocock-skills-deck";
|
package/dist/gate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// AUTO-GENERATED by node
|
|
1
|
+
// AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/gate.ts
|
|
2
2
|
import { assertPluginId } from "./config.js";
|
|
3
3
|
const EVENT_LEVELS = ["error", "warn", "info", "debug"];
|
|
4
4
|
const EVENT_KINDS = ["resident", "ondemand", "selfmon"];
|
package/dist/host.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// AUTO-GENERATED by node
|
|
1
|
+
// AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/host.ts
|
|
2
2
|
import { realpath } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join, sep } from "node:path";
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import { manualCommand } from "./commands.js";
|
|
14
14
|
import { validVersion } from "./service.js";
|
|
15
15
|
import { createUpdateDiskPorts, createUpdateExecutor } from "./store.js";
|
|
16
|
-
import { containingPackage, createUpdateReader, defaultHomeDir } from "./reader.js";
|
|
16
|
+
import { containingPackage, createUpdateReader, defaultHomeDir, resolveProfileName } from "./reader.js";
|
|
17
17
|
import { buildPhoneName, buildPhoneNames as buildPhoneNames2, resolveUpdateConfig as resolveUpdateConfig2 } from "./config.js";
|
|
18
18
|
import { parseEventListManifest, checkEventFields, checkEventCounts } from "./gate.js";
|
|
19
19
|
import { containingPackage as containingPackage2, defaultHomeDir as defaultHomeDir2, profileNameValid, registrySpec } from "./reader.js";
|
|
@@ -29,9 +29,22 @@ function ctxService(ctx, name) {
|
|
|
29
29
|
return void 0;
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
function detectEnvironmentKind(ctx) {
|
|
32
|
+
function detectEnvironmentKind(ctx, profileName) {
|
|
33
33
|
const profiles = ctxService(ctx, "desktopProfiles");
|
|
34
|
-
|
|
34
|
+
if (profiles !== void 0 && profiles !== null) return "desktop";
|
|
35
|
+
const manager = ctxService(ctx, "pluginManager");
|
|
36
|
+
if (hasInstallBundle(manager) && cliRefusesProfile(profileName)) return "desktop-manager";
|
|
37
|
+
return "cli";
|
|
38
|
+
}
|
|
39
|
+
function hasInstallBundle(manager) {
|
|
40
|
+
try {
|
|
41
|
+
return typeof manager?.installBundle === "function";
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function cliRefusesProfile(profileName) {
|
|
47
|
+
return typeof profileName === "string" && profileName.toLowerCase() === "desktop";
|
|
35
48
|
}
|
|
36
49
|
function watchDesktopPnpm(ctx) {
|
|
37
50
|
try {
|
|
@@ -85,7 +98,7 @@ async function getSharedReader(pluginId, config, overrides = {}) {
|
|
|
85
98
|
if (!runningVersion) throw Object.assign(new Error("unknown-profile"), { code: "unknown-profile" });
|
|
86
99
|
const profileDirInput = overrides.profileDir ?? await inferProfileDir(loaded, homeDirDefault, targetPackageName);
|
|
87
100
|
const homeDirInput = overrides.homeDir ?? config.homeDir ?? homeDirDefault;
|
|
88
|
-
const environmentKind = overrides.environmentKind ?? detectEnvironmentKind(overrides.ctx);
|
|
101
|
+
const environmentKind = overrides.environmentKind ?? detectEnvironmentKind(overrides.ctx, resolveProfileName(overrides.profileName, profileDirInput));
|
|
89
102
|
const checkTimeoutMs = overrides.checkTimeoutMs ?? config.checkTimeoutMs;
|
|
90
103
|
const confirmationTtlMs = overrides.confirmationTtlMs ?? config.confirmationTtlMs;
|
|
91
104
|
const installTimeoutMs = overrides.installTimeoutMs ?? config.installTimeoutMs;
|
|
@@ -100,6 +113,7 @@ async function getSharedReader(pluginId, config, overrides = {}) {
|
|
|
100
113
|
subprocess: () => overrides.subprocess ?? ctxService(overrides.ctx, "subprocess"),
|
|
101
114
|
desktopPnpm: () => overrides.desktopPnpm ?? sharedDesktopPnpm,
|
|
102
115
|
desktopProfiles: () => overrides.desktopProfiles ?? ctxService(overrides.ctx, "desktopProfiles"),
|
|
116
|
+
pluginManager: () => overrides.pluginManager ?? ctxService(overrides.ctx, "pluginManager"),
|
|
103
117
|
runtimeExecutable: () => overrides.runtimeExecutable,
|
|
104
118
|
runtimeExecArgs: () => overrides.runtimeExecArgs,
|
|
105
119
|
cliEntry: () => overrides.cliEntry,
|
|
@@ -216,7 +230,7 @@ function createHostUpdate(deps = {}, configInput) {
|
|
|
216
230
|
const ctx = deps.ctx ?? null;
|
|
217
231
|
if (deps.desktopPnpm) sharedDesktopPnpm = deps.desktopPnpm;
|
|
218
232
|
else watchDesktopPnpm(ctx);
|
|
219
|
-
const readerOverrides = { ...deps.readerOverrides ?? {}, ctx };
|
|
233
|
+
const readerOverrides = { ...deps.readerOverrides ?? {}, ctx, ...deps.pluginManager ? { pluginManager: deps.pluginManager } : {} };
|
|
220
234
|
const phoneNames = buildPhoneNames(config.prefix);
|
|
221
235
|
async function readStatus(args) {
|
|
222
236
|
const reader = await getSharedReader(pluginId, config, {
|
package/dist/ports.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// AUTO-GENERATED by node
|
|
2
|
-
const PORTS_SOURCE = "
|
|
1
|
+
// AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/ports.ts
|
|
2
|
+
const PORTS_SOURCE = "src/ports.ts";
|
|
3
3
|
export {
|
|
4
4
|
PORTS_SOURCE
|
|
5
5
|
};
|
package/dist/reader.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// AUTO-GENERATED by node
|
|
1
|
+
// AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/reader.ts
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { readFile, realpath, stat } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
@@ -37,6 +37,9 @@ async function containingPackage(filename, name) {
|
|
|
37
37
|
directory = parent;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
function resolveProfileName(profileName, profileDir) {
|
|
41
|
+
return profileName ?? basename(profileDir);
|
|
42
|
+
}
|
|
40
43
|
function profileNameValid(name) {
|
|
41
44
|
return typeof name === "string" && name.length > 0 && Buffer.byteLength(name) <= 255 && !name.startsWith("-") && ![".", "..", "node_modules"].includes(name) && !/[\\/\x00-\x1f\x7f<>:"|?*]/u.test(name);
|
|
42
45
|
}
|
|
@@ -114,7 +117,7 @@ function createUpdateReader(options) {
|
|
|
114
117
|
installationKey: null,
|
|
115
118
|
eligible: false
|
|
116
119
|
};
|
|
117
|
-
const profileName = options.profileName
|
|
120
|
+
const profileName = resolveProfileName(options.profileName, profileDirInput);
|
|
118
121
|
result.profileName = profileName;
|
|
119
122
|
if (!profileNameValid(profileName)) {
|
|
120
123
|
result.blockedReason = "unknown-profile";
|
|
@@ -189,5 +192,6 @@ export {
|
|
|
189
192
|
createUpdateReader,
|
|
190
193
|
defaultHomeDir,
|
|
191
194
|
profileNameValid,
|
|
192
|
-
registrySpec
|
|
195
|
+
registrySpec,
|
|
196
|
+
resolveProfileName
|
|
193
197
|
};
|
package/dist/service.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// AUTO-GENERATED by node
|
|
1
|
+
// AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/service.ts
|
|
2
2
|
const PACKAGE_NAME = "dsh-mattpocock-skills-deck";
|
|
3
3
|
const NPM_REGISTRY = "https://registry.npmjs.org/";
|
|
4
4
|
const CHECK_TIMEOUT_MS = 1e4;
|
|
@@ -275,13 +275,21 @@ function createUpdateCore(ports) {
|
|
|
275
275
|
activeJobId = doing.id;
|
|
276
276
|
await saveJob(doing);
|
|
277
277
|
const env = await ports.readInstalled();
|
|
278
|
-
if (env.installedVersion !== job.targetVersion)
|
|
279
|
-
|
|
278
|
+
if (env.installedVersion !== job.targetVersion) {
|
|
279
|
+
throw Object.assign(updateError("install-failed"), {
|
|
280
|
+
detail: `\u88C5\u5B8C\u6821\u9A8C\u6CA1\u8FC7\uFF1A\u78C1\u76D8\u4E0A\u662F ${env.installedVersion ?? "\u672A\u77E5"}\uFF0C\u76EE\u6807\u662F ${job.targetVersion ?? "\u672A\u77E5"}`
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (env.blockedReason && env.blockedReason !== "pending-restart") {
|
|
284
|
+
throw Object.assign(updateError("install-failed"), { detail: `\u88C5\u5B8C\u6821\u9A8C\u6CA1\u8FC7\uFF1A\u73AF\u5883\u62A5\u544A ${env.blockedReason}` });
|
|
285
|
+
}
|
|
280
286
|
const done = { ...doing, state: "restart-required", message: null };
|
|
281
287
|
await saveJob(done);
|
|
282
288
|
} catch (error) {
|
|
283
289
|
const code = error?.code;
|
|
284
|
-
const
|
|
290
|
+
const base = code === "installation-changed" || code === "registry-conflict" || code === "install-failed" ? String(code) : "install-failed";
|
|
291
|
+
const detail = error?.detail;
|
|
292
|
+
const message = typeof detail === "string" && detail ? `${base}: ${detail}` : base;
|
|
285
293
|
const failedJob = { ...job, state: "failed", message, requestId: job.requestId ?? null };
|
|
286
294
|
try {
|
|
287
295
|
await saveJob(failedJob);
|
package/dist/store.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// AUTO-GENERATED by node
|
|
1
|
+
// AUTO-GENERATED by node build.mjs — DO NOT EDIT. Source: src/store.ts
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { mkdir, open, readFile, rename, realpath, stat, unlink, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
@@ -294,6 +294,126 @@ async function runDesktopService(recipe, parts) {
|
|
|
294
294
|
if (code !== 0) throw withExit(fail("install-failed"), code);
|
|
295
295
|
return code;
|
|
296
296
|
}
|
|
297
|
+
const DETAIL_MAX = 300;
|
|
298
|
+
const MANAGER_OK = ["applied", "restart-required", "overridden"];
|
|
299
|
+
function withDetail(error, detail) {
|
|
300
|
+
if (detail) error.detail = detail;
|
|
301
|
+
return error;
|
|
302
|
+
}
|
|
303
|
+
const ABSOLUTE_PATH_RE = /[A-Za-z]:\\[^\s"']*|(^|\s)\/[^\s"']+/g;
|
|
304
|
+
function sanitizeDetail(text) {
|
|
305
|
+
const flat = String(text ?? "").replace(/\s+/g, " ").trim();
|
|
306
|
+
if (!flat) return "";
|
|
307
|
+
const noPath = flat.replace(ABSOLUTE_PATH_RE, (_whole, lead) => `${lead || ""}<\u8DEF\u5F84>`);
|
|
308
|
+
return noPath.length > DETAIL_MAX ? `${noPath.slice(0, DETAIL_MAX)}\u2026` : noPath;
|
|
309
|
+
}
|
|
310
|
+
function detailText(error) {
|
|
311
|
+
if (typeof error === "string") return sanitizeDetail(error);
|
|
312
|
+
if (error instanceof Error) return sanitizeDetail(error.message);
|
|
313
|
+
if (error && typeof error === "object") {
|
|
314
|
+
const shape = error;
|
|
315
|
+
const code = typeof shape.code === "string" ? shape.code : "";
|
|
316
|
+
const said = typeof shape.diagnostic === "string" ? shape.diagnostic : typeof shape.message === "string" ? shape.message : "";
|
|
317
|
+
const line = [code, said].filter(Boolean).join(": ");
|
|
318
|
+
if (line) return sanitizeDetail(line);
|
|
319
|
+
try {
|
|
320
|
+
return sanitizeDetail(JSON.stringify(error));
|
|
321
|
+
} catch {
|
|
322
|
+
return sanitizeDetail(String(error));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return error === void 0 || error === null ? "" : sanitizeDetail(String(error));
|
|
326
|
+
}
|
|
327
|
+
function managerSpecOf(pluginArgs) {
|
|
328
|
+
if (!Array.isArray(pluginArgs) || pluginArgs.length !== 2) return null;
|
|
329
|
+
const [verb, spec] = pluginArgs;
|
|
330
|
+
if (verb !== "add" || typeof spec !== "string" || !spec || spec.startsWith("-")) return null;
|
|
331
|
+
return spec;
|
|
332
|
+
}
|
|
333
|
+
async function raceManager(running, timeoutMs, requestCancel) {
|
|
334
|
+
const settled = Promise.resolve(running).then((value) => ({ timedOut: false, value }));
|
|
335
|
+
if (!(typeof timeoutMs === "number" && timeoutMs > 0)) return await settled;
|
|
336
|
+
const TIMEOUT = /* @__PURE__ */ Symbol("install-timeout");
|
|
337
|
+
let timer = null;
|
|
338
|
+
const deadline = new Promise((settle) => {
|
|
339
|
+
timer = setTimeout(() => settle(TIMEOUT), timeoutMs);
|
|
340
|
+
});
|
|
341
|
+
let winner;
|
|
342
|
+
try {
|
|
343
|
+
winner = await Promise.race([settled, deadline]);
|
|
344
|
+
} finally {
|
|
345
|
+
if (timer !== null) {
|
|
346
|
+
try {
|
|
347
|
+
clearTimeout(timer);
|
|
348
|
+
} catch {
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (winner !== TIMEOUT) return winner;
|
|
353
|
+
await requestCancel();
|
|
354
|
+
return { timedOut: true };
|
|
355
|
+
}
|
|
356
|
+
async function settleWithin(promise, graceMs) {
|
|
357
|
+
if (!(typeof graceMs === "number" && graceMs > 0)) return null;
|
|
358
|
+
const EXPIRED = /* @__PURE__ */ Symbol("grace-expired");
|
|
359
|
+
let timer = null;
|
|
360
|
+
const deadline = new Promise((settle) => {
|
|
361
|
+
timer = setTimeout(() => settle(EXPIRED), graceMs);
|
|
362
|
+
});
|
|
363
|
+
try {
|
|
364
|
+
const winner = await Promise.race([promise, deadline]);
|
|
365
|
+
return winner === EXPIRED ? null : winner;
|
|
366
|
+
} finally {
|
|
367
|
+
if (timer !== null) {
|
|
368
|
+
try {
|
|
369
|
+
clearTimeout(timer);
|
|
370
|
+
} catch {
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async function runDesktopManager(recipe, parts) {
|
|
376
|
+
try {
|
|
377
|
+
const manager = part(parts.pluginManager ?? null);
|
|
378
|
+
if (!manager || typeof manager.installBundle !== "function") throw fail("install-failed");
|
|
379
|
+
const spec = managerSpecOf(recipe.pluginArgs);
|
|
380
|
+
if (!spec) throw fail("install-failed");
|
|
381
|
+
const requestId = randomUUID();
|
|
382
|
+
const registry = part(parts.registryUrl ?? "");
|
|
383
|
+
const options = { requestId };
|
|
384
|
+
if (typeof registry === "string" && registry) options.registry = registry;
|
|
385
|
+
const running = Promise.resolve(manager.installBundle(spec, options));
|
|
386
|
+
const outcome = await raceManager(running, recipe.timeoutMs, async () => {
|
|
387
|
+
const cancel = manager.cancelInstall;
|
|
388
|
+
if (typeof cancel !== "function") return "not-running";
|
|
389
|
+
try {
|
|
390
|
+
const answer = await cancel.call(manager, requestId);
|
|
391
|
+
const status = answer && typeof answer.status === "string" ? answer.status : "";
|
|
392
|
+
return status === "cancelled" || status === "too-late" ? status : "not-running";
|
|
393
|
+
} catch {
|
|
394
|
+
return "not-running";
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
let result;
|
|
398
|
+
if (!outcome.timedOut) result = outcome.value;
|
|
399
|
+
else {
|
|
400
|
+
const settled = await settleWithin(running, TERMINATION_GRACE_MS);
|
|
401
|
+
if (!settled) {
|
|
402
|
+
void running.catch(() => {
|
|
403
|
+
});
|
|
404
|
+
throw withDetail(fail("install-failed"), "\u5B89\u88C5\u8D85\u65F6\uFF0C\u5DF2\u8BF7\u6C42\u5BBF\u4E3B\u53D6\u6D88");
|
|
405
|
+
}
|
|
406
|
+
result = settled;
|
|
407
|
+
}
|
|
408
|
+
const application = result && typeof result.application === "string" ? result.application : "";
|
|
409
|
+
if (MANAGER_OK.includes(application)) return 0;
|
|
410
|
+
if (application === "cancelled") throw withDetail(fail("install-failed"), "\u5B89\u88C5\u5DF2\u88AB\u53D6\u6D88");
|
|
411
|
+
throw withDetail(fail("install-failed"), detailText(result && result.error !== void 0 ? result.error : `application: ${application || "unknown"}`));
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (error?.code === "install-failed") throw error;
|
|
414
|
+
throw withDetail(fail("install-failed"), detailText(error));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
297
417
|
async function runCliProcess(recipe, parts) {
|
|
298
418
|
const executable = part(parts.runtimeExecutable) || (typeof process !== "undefined" ? process.execPath : "");
|
|
299
419
|
const execArgs = part(parts.runtimeExecArgs) ?? (typeof process !== "undefined" && Array.isArray(process.execArgv) ? process.execArgv : []);
|
|
@@ -313,6 +433,11 @@ async function runCliProcess(recipe, parts) {
|
|
|
313
433
|
if (outcome.timedOut || outcome.exitCode !== 0) throw withExit(fail("install-failed"), outcome.exitCode);
|
|
314
434
|
return outcome.exitCode;
|
|
315
435
|
}
|
|
436
|
+
const INSTALL_RUNNERS = {
|
|
437
|
+
"desktop-service": runDesktopService,
|
|
438
|
+
"desktop-manager": runDesktopManager,
|
|
439
|
+
"cli-process": runCliProcess
|
|
440
|
+
};
|
|
316
441
|
function createUpdateExecutor(parts = {}) {
|
|
317
442
|
return async function runInstall(args = {}) {
|
|
318
443
|
const recipe = installRecipe({
|
|
@@ -327,11 +452,17 @@ function createUpdateExecutor(parts = {}) {
|
|
|
327
452
|
let exitCode = null;
|
|
328
453
|
try {
|
|
329
454
|
if (!recipe) throw fail("install-failed");
|
|
330
|
-
|
|
455
|
+
const runner = INSTALL_RUNNERS[recipe.route] ?? null;
|
|
456
|
+
if (!runner) throw fail("install-failed");
|
|
457
|
+
exitCode = await runner(recipe, parts);
|
|
331
458
|
emitInstall(parts, recipe, true, exitCode, Date.now() - startedAt);
|
|
332
459
|
} catch (error) {
|
|
333
460
|
emitInstall(parts, recipe, false, error?.exitCode ?? exitCode, Date.now() - startedAt);
|
|
334
|
-
|
|
461
|
+
const detail = error?.detail;
|
|
462
|
+
throw Object.assign(fail("install-failed"), {
|
|
463
|
+
debug: String(error?.stack || error),
|
|
464
|
+
...typeof detail === "string" && detail ? { detail } : {}
|
|
465
|
+
});
|
|
335
466
|
}
|
|
336
467
|
};
|
|
337
468
|
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-update",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "可复用的更新系统装成的 npm 包:任何 DSH 插件照文档能集成检查更新等功能(地图 #579)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "王辰浩",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/FeatherHunter/dsh-plugin-update.git"
|
|
10
|
+
},
|
|
7
11
|
"engines": {
|
|
8
12
|
"node": ">=22"
|
|
9
13
|
},
|
|
@@ -26,9 +30,12 @@
|
|
|
26
30
|
"scripts": {
|
|
27
31
|
"typecheck": "tsc -p tsconfig.json",
|
|
28
32
|
"build": "node build.mjs",
|
|
33
|
+
"prepack": "npm run build",
|
|
34
|
+
"pretest": "npm run build",
|
|
29
35
|
"test": "node --test tests/*.test.mjs"
|
|
30
36
|
},
|
|
31
37
|
"devDependencies": {
|
|
38
|
+
"esbuild": "^0.28.2",
|
|
32
39
|
"typescript": "5.6.3"
|
|
33
40
|
}
|
|
34
41
|
}
|