dsh-plugin-update-checker 0.1.4 → 0.1.5
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 +13 -1
- package/README.zh-CN.md +11 -1
- package/lib/host/registry.js +79 -23
- package/lib/host/scan.js +4 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -30,7 +30,19 @@ The check is on demand: click the button, the host scans, and the table fills in
|
|
|
30
30
|
|
|
31
31
|
## 📦 Install
|
|
32
32
|
|
|
33
|
-
### Option A — from
|
|
33
|
+
### Option A — from npm (recommended)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
dsh install-plugin dsh-plugin-update-checker
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Installing from the registry matters for this plugin in particular: a `github:`
|
|
40
|
+
install is pinned to a commit in the profile lockfile, so re-running the same
|
|
41
|
+
spec never picks up a newer release, and the panel reports it as *Unsupported*
|
|
42
|
+
(it can only compare registry versions). From `0.1.4` on, plain version numbers
|
|
43
|
+
resolve normally and the panel's own **Update** button works.
|
|
44
|
+
|
|
45
|
+
### Option B — from this GitHub repository
|
|
34
46
|
|
|
35
47
|
1. Open the sidebar **Plugins** page → **Add plugin**.
|
|
36
48
|
2. In the source field, enter one of these install specs (the launcher resolves them through `installBundle`, the same primitive the GUI uses):
|
package/README.zh-CN.md
CHANGED
|
@@ -30,7 +30,17 @@
|
|
|
30
30
|
|
|
31
31
|
## 📦 安装
|
|
32
32
|
|
|
33
|
-
### 方式 A ——
|
|
33
|
+
### 方式 A —— 从 npm 安装(推荐)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
dsh install-plugin dsh-plugin-update-checker
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
从注册表安装对这个插件**尤其值得**:用 `github:` 安装会被 profile 锁文件钉在某个
|
|
40
|
+
commit 上,重跑同样的 spec 不会拿到新版本,面板也只能把它报成 *不受支持*
|
|
41
|
+
(它只会比对注册表版本)。`0.1.4` 之后按普通版本号解析,面板自带的「更新」按钮就能用。
|
|
42
|
+
|
|
43
|
+
### 方式 B —— 从本 GitHub 仓库直接安装
|
|
34
44
|
|
|
35
45
|
1. 打开右侧栏 **插件(Plugins)** 页面 → **添加插件**。
|
|
36
46
|
2. 在来源框里填入下面任意一种安装描述符(启动器通过 `installBundle` 解析,和 GUI 用的是同一个原语):
|
package/lib/host/registry.js
CHANGED
|
@@ -95,41 +95,94 @@ function readRegistryFromNpmrc(file) {
|
|
|
95
95
|
* @param {AbortSignal} signal - request deadline.
|
|
96
96
|
* @returns {Promise<{ result: object, registry: string }>} the winning answer.
|
|
97
97
|
*/
|
|
98
|
-
export async function probeAnyRegistry(name, registries, signal) {
|
|
98
|
+
export async function probeAnyRegistry(name, registries, signal, installedVersion) {
|
|
99
99
|
// Start every candidate at once and settle on the first *successful* answer,
|
|
100
100
|
// in completion order rather than list order. Awaiting in list order would let
|
|
101
101
|
// a slow first registry delay an already-finished mirror — exactly the case
|
|
102
102
|
// this exists to fix.
|
|
103
103
|
if (registries.length === 0) return { result: { error: 'no registry configured' }, registry: undefined }
|
|
104
|
-
|
|
104
|
+
|
|
105
|
+
const probes = registries.map((registry) =>
|
|
106
|
+
probeRegistry(name, registry, signal).then(
|
|
107
|
+
(result) => ({ result, registry }),
|
|
108
|
+
(err) => ({ result: { error: `registry unreachable: ${String(err?.message ?? err)}` }, registry }),
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
// First success wins, so the common case stays as fast as the fastest mirror.
|
|
113
|
+
const first = await new Promise((resolve) => {
|
|
114
|
+
let done = false
|
|
105
115
|
let failures = 0
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
settled = true
|
|
113
|
-
resolve({ result, registry })
|
|
116
|
+
for (const probe of probes) {
|
|
117
|
+
probe.then((answer) => {
|
|
118
|
+
if (done) return
|
|
119
|
+
if (answer.result.error === undefined) {
|
|
120
|
+
done = true
|
|
121
|
+
resolve(answer)
|
|
114
122
|
return
|
|
115
123
|
}
|
|
116
|
-
lastFailure = { result, registry }
|
|
117
124
|
failures += 1
|
|
118
|
-
if (failures ===
|
|
119
|
-
|
|
120
|
-
resolve(
|
|
121
|
-
}
|
|
122
|
-
}, (err) => {
|
|
123
|
-
if (settled) return
|
|
124
|
-
lastFailure = { result: { error: `registry unreachable: ${String(err?.message ?? err)}` }, registry }
|
|
125
|
-
failures += 1
|
|
126
|
-
if (failures === registries.length) {
|
|
127
|
-
settled = true
|
|
128
|
-
resolve(lastFailure)
|
|
125
|
+
if (failures === probes.length) {
|
|
126
|
+
done = true
|
|
127
|
+
resolve(undefined)
|
|
129
128
|
}
|
|
130
129
|
})
|
|
131
130
|
}
|
|
132
131
|
})
|
|
132
|
+
|
|
133
|
+
if (first === undefined) {
|
|
134
|
+
const all = await Promise.all(probes)
|
|
135
|
+
return all[all.length - 1]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// A fast mirror can be stale: it may still carry the placeholder version npm
|
|
139
|
+
// creates for a brand-new package, or lag behind the version we already have
|
|
140
|
+
// installed. Taking that at face value reports "up to date" for something a
|
|
141
|
+
// second registry knows better, which is the one verdict this plugin must not
|
|
142
|
+
// get wrong. When the first answer looks like that, wait for the rest (they
|
|
143
|
+
// are already in flight) and keep the highest real version.
|
|
144
|
+
if (!isSuspect(first.result, installedVersion)) return first
|
|
145
|
+
const all = await Promise.all(probes)
|
|
146
|
+
const best = bestAnswer(all.filter((answer) => answer.result.error === undefined))
|
|
147
|
+
return best ?? first
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** A placeholder version npm creates while a package has no real release yet. */
|
|
151
|
+
function isPlaceholder(version, result) {
|
|
152
|
+
return result?.stubLatest === true && version !== undefined
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Whether an answer should not be trusted as the final word.
|
|
157
|
+
* @param result - one registry's probe result.
|
|
158
|
+
* @param installedVersion - the version on disk, when known.
|
|
159
|
+
*/
|
|
160
|
+
function isSuspect(result, installedVersion) {
|
|
161
|
+
const latest = result?.latest
|
|
162
|
+
if (latest === undefined) return false
|
|
163
|
+
if (isPlaceholder(latest, result)) return true
|
|
164
|
+
// A registry claiming something older than what is installed is stale by
|
|
165
|
+
// definition; the installed version came from a registry in the first place.
|
|
166
|
+
return installedVersion !== undefined
|
|
167
|
+
&& semver.valid(installedVersion) !== null
|
|
168
|
+
&& semver.valid(latest) !== null
|
|
169
|
+
&& semver.lt(latest, installedVersion)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Pick the highest real release among several answers, ignoring placeholders. */
|
|
173
|
+
function bestAnswer(answers) {
|
|
174
|
+
const real = answers.filter((answer) => !isPlaceholder(answer.result.latest, answer.result))
|
|
175
|
+
const pool = real.length > 0 ? real : answers
|
|
176
|
+
let best
|
|
177
|
+
for (const answer of pool) {
|
|
178
|
+
const latest = answer.result.latest
|
|
179
|
+
if (latest === undefined) continue
|
|
180
|
+
if (best === undefined) { best = answer; continue }
|
|
181
|
+
if (semver.valid(latest) !== null && semver.valid(best.result.latest) !== null) {
|
|
182
|
+
if (semver.gt(latest, best.result.latest)) best = answer
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return best
|
|
133
186
|
}
|
|
134
187
|
|
|
135
188
|
/** Probe one npm registry for a package's published versions. */
|
|
@@ -143,7 +196,10 @@ async function probeRegistry(name, registry, signal) {
|
|
|
143
196
|
const distTags = data?.['dist-tags']
|
|
144
197
|
const versions = Array.isArray(data?.versions) ? data.versions : Object.keys(data?.versions ?? {})
|
|
145
198
|
if (distTags?.latest === undefined && versions.length === 0) return { error: 'package not found on registry' }
|
|
146
|
-
|
|
199
|
+
// npm writes a stub version (0.0.0-stage) for a package whose first real
|
|
200
|
+
// release has not landed yet; it is a placeholder, not a release.
|
|
201
|
+
const stubLatest = distTags?.latest !== undefined && data?.versions?.[distTags.latest]?.stub === true
|
|
202
|
+
return { latest: distTags?.latest, versions, stubLatest }
|
|
147
203
|
} catch (err) {
|
|
148
204
|
if (err?.name === 'AbortError') return { error: 'request aborted' }
|
|
149
205
|
return { error: `registry unreachable: ${String(err?.message ?? err)}` }
|
package/lib/host/scan.js
CHANGED
|
@@ -109,7 +109,10 @@ export async function scanBundle(bundle, profileDir, dshVersion, exemptions, sig
|
|
|
109
109
|
// race the configured registries so a slow public registry cannot dominate.
|
|
110
110
|
const declared = typeof bundle.registry === 'string' && bundle.registry !== '' ? bundle.registry : ''
|
|
111
111
|
const candidates = declared !== '' ? [declared] : registries
|
|
112
|
-
|
|
112
|
+
// The installed version lets the race spot a stale mirror instead of
|
|
113
|
+
// reporting "up to date" from an answer older than what is on disk.
|
|
114
|
+
const { result: remote, registry: usedRegistry } =
|
|
115
|
+
await probeAnyRegistry(name, candidates, signal, installedVersion ?? manifest?.version)
|
|
113
116
|
row.update = classifyUpdate(installedVersion ?? manifest?.version, remote)
|
|
114
117
|
// Surface the registry the answer came from so the one-click update installs
|
|
115
118
|
// from the same place the version was read (the client passes it through).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-update-checker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "A DeepSeek Harness plugin that adds a \"Check updates\" button to the Plugins settings section: it scans every installed plugin bundle for published updates and evaluates its peer-dependency compatibility against the running DSH runtime.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek",
|
|
@@ -59,6 +59,6 @@
|
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
61
|
"scripts": {
|
|
62
|
-
"test": "node test/layering.test.mjs && node test/client-contract.test.mjs && node test/profile-argv.test.mjs && node test/registry-race.test.mjs && node test/runtime-check.test.mjs && node test/compat-parity.test.mjs && node test/scan-route.test.mjs && node test/host-load.test.mjs"
|
|
62
|
+
"test": "node test/layering.test.mjs && node test/client-contract.test.mjs && node test/profile-argv.test.mjs && node test/registry-race.test.mjs && node test/registry-staleness.test.mjs && node test/runtime-check.test.mjs && node test/compat-parity.test.mjs && node test/scan-route.test.mjs && node test/host-load.test.mjs"
|
|
63
63
|
}
|
|
64
64
|
}
|