dsh-plugin-update-checker 0.0.0-stage → 0.1.4
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/LICENSE +21 -0
- package/README.md +118 -2
- package/README.zh-CN.md +113 -0
- package/cordis.patch.yml +14 -0
- package/lib/client.js +429 -0
- package/lib/host/dsh.js +158 -0
- package/lib/host/profile.js +261 -0
- package/lib/host/registry.js +195 -0
- package/lib/host/runtime.js +143 -0
- package/lib/host/scan.js +123 -0
- package/lib/host/update-checker.js +90 -0
- package/lib/index.js +23 -0
- package/package.json +62 -4
package/lib/host/scan.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-update-checker — host/scan.js
|
|
3
|
+
*
|
|
4
|
+
* 由 lib/host/update-checker.js 按下述职责拆出(函数体逐字保留):
|
|
5
|
+
* scanBundle
|
|
6
|
+
*/
|
|
7
|
+
import { lstatSync } from 'node:fs'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
import { isRuntimePackage, evaluatePluginCompatibility } from './dsh.js'
|
|
10
|
+
import { readManifest, findBundleDir, KNOWN_REPOS, resolveSourceLink } from './profile.js'
|
|
11
|
+
import { probeAnyRegistry, classifyUpdate } from './registry.js'
|
|
12
|
+
|
|
13
|
+
export async function scanBundle(bundle, profileDir, dshVersion, exemptions, signal, repositories = {}, registries = ['https://registry.npmjs.org']) {
|
|
14
|
+
const name = bundle.name
|
|
15
|
+
const installedVersion = bundle.version ?? null
|
|
16
|
+
|
|
17
|
+
// The @deepseek-ai/dsh* packages are the runtime, symlinked into the shared
|
|
18
|
+
// tree from the runtime install. They are not user plugins and must not be
|
|
19
|
+
// treated as source-links or independently updatable bundles. The runtime is
|
|
20
|
+
// checked once, as a unit, by checkDshRuntime (which the route appends as the
|
|
21
|
+
// 'dsh-runtime' row). Report the runtime packages themselves as a thin,
|
|
22
|
+
// inert row so they neither blank the report nor invite a misleading
|
|
23
|
+
// "update from the repository" action.
|
|
24
|
+
if (isRuntimePackage(name)) {
|
|
25
|
+
return {
|
|
26
|
+
name,
|
|
27
|
+
installedVersion,
|
|
28
|
+
installed: bundle.installed === true,
|
|
29
|
+
linked: false,
|
|
30
|
+
description: bundle.description ?? bundle.meta?.description ?? null,
|
|
31
|
+
compatibility: { status: 'compatible' },
|
|
32
|
+
update: { status: 'part-of-runtime', error: 'part of the DSH runtime; checked as a unit' },
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const row = {
|
|
36
|
+
name,
|
|
37
|
+
installedVersion,
|
|
38
|
+
installed: bundle.installed === true,
|
|
39
|
+
linked: false,
|
|
40
|
+
description: bundle.description ?? bundle.meta?.description ?? null,
|
|
41
|
+
compatibility: { status: 'compatible' },
|
|
42
|
+
update: { status: 'unknown', error: 'not scanned' },
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Compatibility (no network needed).
|
|
46
|
+
// describeBundle already resolved both, so reuse them when present: a
|
|
47
|
+
// scan of N bundles used to resolve every directory and parse every
|
|
48
|
+
// package.json twice.
|
|
49
|
+
const dir = bundle.dir !== undefined ? bundle.dir : findBundleDir(name, profileDir)
|
|
50
|
+
// A link:-installed bundle (symlink into a source tree) is not published to a
|
|
51
|
+
// registry, so installBundle would replace the developer's symlink with a
|
|
52
|
+
// published copy and break their live-edit loop. Flag it so the UI disables
|
|
53
|
+
// the one-click update and instead links to the source.
|
|
54
|
+
try { if (dir && lstatSync(dir).isSymbolicLink()) row.linked = true } catch { /* keep false */ }
|
|
55
|
+
const manifest = bundle.manifest !== undefined
|
|
56
|
+
? bundle.manifest
|
|
57
|
+
: (dir ? await readManifest(join(dir, 'package.json')) : undefined)
|
|
58
|
+
// Always evaluate: the launcher does. Gating on peerDependencies meant a
|
|
59
|
+
// manifest that declares only engines.dsh was never evaluated here, so the
|
|
60
|
+
// row kept its default "compatible" even when the launcher would skip it.
|
|
61
|
+
if (manifest) {
|
|
62
|
+
try {
|
|
63
|
+
const issue = evaluatePluginCompatibility(manifest, exemptions, dshVersion)
|
|
64
|
+
if (issue) {
|
|
65
|
+
row.compatibility = {
|
|
66
|
+
status: issue.exempted ? 'compatible' : 'incompatible',
|
|
67
|
+
peers: issue.peers,
|
|
68
|
+
exempted: issue.exempted === true,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
row.compatibility = { status: 'unknown', error: 'could not evaluate compatibility' }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Update availability.
|
|
77
|
+
if (!bundle.installed && bundle.optional) {
|
|
78
|
+
row.update = { status: 'unsupported', error: 'shipped by the installation' }
|
|
79
|
+
return row
|
|
80
|
+
}
|
|
81
|
+
// Listed in dsh.profile.bundles but absent from disk. Probing the registry
|
|
82
|
+
// would compare the published version against the 0.0.0 fallback and report
|
|
83
|
+
// "update available" for something that is not installed at all, so say so
|
|
84
|
+
// instead of inventing a phantom upgrade.
|
|
85
|
+
if (!bundle.installed) {
|
|
86
|
+
row.update = { status: 'unsupported', error: 'listed in the profile but not installed' }
|
|
87
|
+
row.compatibility = { status: 'unknown', error: 'no manifest to evaluate' }
|
|
88
|
+
return row
|
|
89
|
+
}
|
|
90
|
+
// Link:-installed bundles publish nothing to a registry; probing it only
|
|
91
|
+
// yields a misleading 404. Skip the network and point the user at the source.
|
|
92
|
+
if (row.linked && dir) {
|
|
93
|
+
const src = resolveSourceLink(dir, repositories[name] ?? KNOWN_REPOS[name])
|
|
94
|
+
const link = src.repoUrl
|
|
95
|
+
? src.repoUrl + '/tree/' + src.ref + (src.subpath ? '/' + src.subpath : '')
|
|
96
|
+
: null
|
|
97
|
+
row.source = {
|
|
98
|
+
dir: src.dir,
|
|
99
|
+
repoUrl: src.repoUrl,
|
|
100
|
+
ref: src.ref,
|
|
101
|
+
subpath: src.subpath,
|
|
102
|
+
link,
|
|
103
|
+
}
|
|
104
|
+
row.update = { status: 'source', error: 'installed from source; update from the repository' }
|
|
105
|
+
return row
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
// A manifest-declared registry is authoritative for that package; otherwise
|
|
109
|
+
// race the configured registries so a slow public registry cannot dominate.
|
|
110
|
+
const declared = typeof bundle.registry === 'string' && bundle.registry !== '' ? bundle.registry : ''
|
|
111
|
+
const candidates = declared !== '' ? [declared] : registries
|
|
112
|
+
const { result: remote, registry: usedRegistry } = await probeAnyRegistry(name, candidates, signal)
|
|
113
|
+
row.update = classifyUpdate(installedVersion ?? manifest?.version, remote)
|
|
114
|
+
// Surface the registry the answer came from so the one-click update installs
|
|
115
|
+
// from the same place the version was read (the client passes it through).
|
|
116
|
+
if (row.update.status !== 'unknown' && usedRegistry) row.update.registry = usedRegistry
|
|
117
|
+
} catch (err) {
|
|
118
|
+
row.update = { status: 'unknown', error: String(err?.message ?? err) }
|
|
119
|
+
}
|
|
120
|
+
return row
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Read one manifest synchronously (used for lightweight bundle enumeration). */
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-update-checker — host half entry.
|
|
3
|
+
*
|
|
4
|
+
* 这里只做装配:解析运行时版本 → 枚举 profile 的 bundles → 逐包扫描 → 回一行 JSON。
|
|
5
|
+
* 具体职责见同目录:dsh.js(DSH 适配)、profile.js(目录与清单)、registry.js(源与探测)、
|
|
6
|
+
* runtime.js(运行时更新源)、scan.js(单包扫描)。
|
|
7
|
+
*/
|
|
8
|
+
import { getDshRuntimeVersion, readProfileVersionExemptions } from './dsh.js'
|
|
9
|
+
import { activeProfileDir, KNOWN_REPOS, listBundles } from './profile.js'
|
|
10
|
+
import { resolveRegistries } from './registry.js'
|
|
11
|
+
import { checkDshRuntime } from './runtime.js'
|
|
12
|
+
import { scanBundle } from './scan.js'
|
|
13
|
+
|
|
14
|
+
// 对外导出面保持不变:evaluatePluginCompatibility 随拆分搬到了 dsh.js,
|
|
15
|
+
// 但测试与外部调用方一直从入口取它,所以在这里再导出一次。
|
|
16
|
+
export { evaluatePluginCompatibility } from './dsh.js'
|
|
17
|
+
|
|
18
|
+
export function apply(ctx, _config = {}) {
|
|
19
|
+
const { webServer } = ctx
|
|
20
|
+
const dshVersion = getDshRuntimeVersion()
|
|
21
|
+
const repositories = { ...KNOWN_REPOS, ...(_config?.repositories ?? {}) }
|
|
22
|
+
|
|
23
|
+
ctx.effect(() => webServer.register({
|
|
24
|
+
kind: 'exact',
|
|
25
|
+
path: '/api/plugin-update-checker/scan',
|
|
26
|
+
handler: async (req, res) => {
|
|
27
|
+
if (req.method !== 'POST') {
|
|
28
|
+
res.statusCode = 405
|
|
29
|
+
res.setHeader('content-type', 'application/json')
|
|
30
|
+
res.end(JSON.stringify({ ok: false, error: 'method not allowed' }))
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
const profileDir = activeProfileDir()
|
|
34
|
+
const exemptions = (() => { try { return readProfileVersionExemptions(profileDir) } catch { return {} } })()
|
|
35
|
+
const controller = new AbortController()
|
|
36
|
+
const timeout = setTimeout(() => controller.abort(), 20000)
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Enumerate from the profile's dsh.profile.bundles on disk. The host
|
|
40
|
+
// half used to try `ctx.remote.pluginManager.listBundles()` first, but
|
|
41
|
+
// `remote` is a browser-side service: on the host that access throws
|
|
42
|
+
// "cannot get property ... without inject", the catch swallowed it, and
|
|
43
|
+
// the disk scan ran anyway. The throw was dead weight, not a fallback.
|
|
44
|
+
const bundles = await listBundles(profileDir)
|
|
45
|
+
|
|
46
|
+
// Resolve registries once per scan, not once per bundle, and racing them
|
|
47
|
+
// keeps a slow public registry from dominating a mirror-served host.
|
|
48
|
+
// NOTE: reads configuration, never ctx.remote — that is a browser-side
|
|
49
|
+
// service, and touching it here throws "cannot get property ... without
|
|
50
|
+
// inject" (the host half declares only webServer).
|
|
51
|
+
const registries = resolveRegistries(profileDir)
|
|
52
|
+
|
|
53
|
+
const plugins = await Promise.all(
|
|
54
|
+
bundles.map((b) => scanBundle(b, profileDir, dshVersion, exemptions, controller.signal, repositories, registries)
|
|
55
|
+
.catch((err) => ({
|
|
56
|
+
name: b.name,
|
|
57
|
+
installedVersion: b.version ?? null,
|
|
58
|
+
compatibility: { status: 'unknown', error: String(err?.message ?? err) },
|
|
59
|
+
update: { status: 'unknown', error: String(err?.message ?? err) },
|
|
60
|
+
})))
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// The runtime is not a bundle in the profile, so it is never in
|
|
64
|
+
// `bundles`. Check it explicitly as one unit so the panel can answer
|
|
65
|
+
// "is my DSH itself up to date?" — the question the old panel silently
|
|
66
|
+
// left unanswered. It is appended, never merged into a plugin row.
|
|
67
|
+
const runtimeRow = await checkDshRuntime(dshVersion, registries, controller.signal)
|
|
68
|
+
.catch((err) => ({ name: 'dsh-runtime', installedVersion: dshVersion, compatibility: { status: 'compatible' }, update: { status: 'unknown', error: String(err?.message ?? err) } }))
|
|
69
|
+
plugins.push(runtimeRow)
|
|
70
|
+
|
|
71
|
+
res.statusCode = 200
|
|
72
|
+
res.setHeader('content-type', 'application/json')
|
|
73
|
+
res.end(JSON.stringify({ ok: true, dshVersion, checkedAt: new Date().toISOString(), plugins }))
|
|
74
|
+
} catch (err) {
|
|
75
|
+
res.statusCode = 500
|
|
76
|
+
res.setHeader('content-type', 'application/json')
|
|
77
|
+
res.end(JSON.stringify({ ok: false, error: String(err?.message ?? err) }))
|
|
78
|
+
} finally {
|
|
79
|
+
clearTimeout(timeout)
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
}), 'plugin-update-checker: scan route')
|
|
83
|
+
|
|
84
|
+
// NOTE: There is deliberately NO restart route here. DSH's own plugin
|
|
85
|
+
// manager never restarts the app (a "restart-required" install result merely
|
|
86
|
+
// means "takes effect at next start"); restarting on this machine is a
|
|
87
|
+
// manual operator action (stop the :3080 listeners, then `dsh restart` or
|
|
88
|
+
// re-launch). The browser half surfaces that as a manual prompt instead of
|
|
89
|
+
// trying to detach-kill its own host process, which proved unreliable.
|
|
90
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-update-checker — Host half entry.
|
|
3
|
+
*
|
|
4
|
+
* Composes the scan route. The browser half (./client.js) registers the
|
|
5
|
+
* "Check updates" tab into the Plugins settings section and calls the route.
|
|
6
|
+
*
|
|
7
|
+
* `inject` lists exactly the services the host logic touches. NOTE: `remote`
|
|
8
|
+
* is a *browser*-side service (injected by dsh-client-ui-plugin-manager into
|
|
9
|
+
* the browser half) — it does NOT exist on the host, so the host half must
|
|
10
|
+
* never declare it. Updating is performed from the browser half via
|
|
11
|
+
* ctx.remote.pluginManager.installBundle, exactly like the GUI "Add plugin"
|
|
12
|
+
* dialog; the host half only serves the read-only scan route here.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { apply as applyUpdateChecker } from './host/update-checker.js'
|
|
16
|
+
|
|
17
|
+
export const name = 'dsh-plugin-update-checker'
|
|
18
|
+
|
|
19
|
+
export const inject = ['webServer']
|
|
20
|
+
|
|
21
|
+
export function apply(ctx, config = {}) {
|
|
22
|
+
applyUpdateChecker(ctx, config)
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-update-checker",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.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
|
+
"keywords": [
|
|
6
|
+
"deepseek",
|
|
7
|
+
"deepseek-harness",
|
|
8
|
+
"dsh-plugin",
|
|
9
|
+
"dsh",
|
|
10
|
+
"plugin-manager",
|
|
11
|
+
"update-checker"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/windrover/dsh-plugin-update-checker#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/windrover/dsh-plugin-update-checker/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/windrover/dsh-plugin-update-checker.git"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "lib/index.js",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": "./lib/index.js",
|
|
26
|
+
"./client": "./lib/client.js",
|
|
27
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"lib/index.js",
|
|
32
|
+
"lib/client.js",
|
|
33
|
+
"lib/host/*.js",
|
|
34
|
+
"cordis.patch.yml",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"README.zh-CN.md"
|
|
38
|
+
],
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"semver": "^7.6.0"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"dsh": ">=0.2.0-0 <0.3.0-0"
|
|
47
|
+
},
|
|
48
|
+
"dsh": {
|
|
49
|
+
"bundle": {
|
|
50
|
+
"patch": "./cordis.patch.yml"
|
|
51
|
+
},
|
|
52
|
+
"client": {
|
|
53
|
+
"inject": [
|
|
54
|
+
"@deepseek-ai/dsh-client-locale",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
56
|
+
"@deepseek-ai/dsh-client-modules"
|
|
57
|
+
],
|
|
58
|
+
"platform": "web"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
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"
|
|
63
|
+
}
|
|
64
|
+
}
|