dsh-plugin-update-checker 0.1.4 → 0.1.6
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/client.js +13 -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/client.js
CHANGED
|
@@ -59,6 +59,7 @@ window.__ModuleLoader__.load({
|
|
|
59
59
|
"updating": "更新中…",
|
|
60
60
|
"updated": "已更新",
|
|
61
61
|
"updateFailed": "更新失败",
|
|
62
|
+
"updateBlocked": "profile 未变更:很可能是发布年龄策略尚未放行该版本。请执行",
|
|
62
63
|
"linked": "源码链接(请从源码更新)",
|
|
63
64
|
"linkedHint": "该插件以源码软链方式安装,无法一键更新;请到源码目录拉取最新代码后重启 DSH。",
|
|
64
65
|
"sourceLink": "前往源码仓库",
|
|
@@ -102,6 +103,7 @@ window.__ModuleLoader__.load({
|
|
|
102
103
|
"updating": "Updating…",
|
|
103
104
|
"updated": "Updated",
|
|
104
105
|
"updateFailed": "Update failed",
|
|
106
|
+
"updateBlocked": "the profile was not modified. Most likely the release-age policy has not admitted that version yet — install it explicitly with",
|
|
105
107
|
"linked": "Source-linked (update from source)",
|
|
106
108
|
"linkedHint": "This plugin is installed as a source symlink and cannot be updated in one click. Pull the latest from its source repo, then restart DSH.",
|
|
107
109
|
"sourceLink": "Open source repository",
|
|
@@ -245,7 +247,17 @@ window.__ModuleLoader__.load({
|
|
|
245
247
|
// still report ok without changing anything (e.g. a bare spec
|
|
246
248
|
// against an already-pinned version), so surface that.
|
|
247
249
|
const changed = value.changed === true;
|
|
248
|
-
if (!changed)
|
|
250
|
+
if (!changed) {
|
|
251
|
+
// ok:false would mean the install failed; ok:true with changed:false
|
|
252
|
+
// means pnpm accepted the command but the profile already satisfied it.
|
|
253
|
+
// The common cause is the profile release-age policy: a version
|
|
254
|
+
// published minutes ago is not admitted until it is old enough, and
|
|
255
|
+
// installBundle does not register the exclusion the App CLI adds.
|
|
256
|
+
// "registry returned no newer version" was misleading: the registry
|
|
257
|
+
// did have it, the local policy refused it.
|
|
258
|
+
const target = typeof latest === "string" && latest !== "" ? `${name}@${latest}` : name;
|
|
259
|
+
throw new Error(`${t("updateBlocked")} dsh install-plugin ${target}`);
|
|
260
|
+
}
|
|
249
261
|
setOutcome((s) => ({ ...s, [name]: { ok: true, restartRequired: application === "restart-required", application } }));
|
|
250
262
|
// When the change is live immediately, re-scan so the row flips
|
|
251
263
|
// to "up to date" instead of still showing "update available".
|
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.6",
|
|
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
|
}
|