dsh-dbhub-live 5.2.0-dev.3 → 5.2.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/AGENTS.md +116 -116
- package/README.en.md +22 -22
- package/README.md +23 -23
- package/doc/ARCHITECTURE.md +108 -108
- package/doc/CLIENT.md +124 -124
- package/doc/DESENSITIZE.md +56 -56
- package/doc/DEVELOPMENT.md +139 -139
- package/doc/IMPACT-MATRIX.md +36 -36
- package/doc/RELEASE.md +93 -93
- package/lib/meta.mjs +267 -267
- package/lib/policy.mjs +92 -92
- package/lib/update.mjs +244 -244
- package/package.json +1 -1
- package/scripts/desensitize-check.mjs +331 -331
- package/scripts/docs-check.mjs +214 -214
- package/scripts/release-preflight.mjs +180 -180
- package/test/meta.test.mjs +234 -234
- package/test/policy.test.mjs +132 -132
- package/test/update.test.mjs +270 -270
package/lib/meta.mjs
CHANGED
|
@@ -1,267 +1,267 @@
|
|
|
1
|
-
// dsh-dbhub-live: version metadata for the Host.
|
|
2
|
-
//
|
|
3
|
-
// Two independent questions live here:
|
|
4
|
-
// 1. which version of THIS plugin is running (its own package.json), and
|
|
5
|
-
// 2. which dsh release is hosting it, and whether that release is new enough
|
|
6
|
-
// for the modern UI contracts (>= 0.2.1 base version).
|
|
7
|
-
//
|
|
8
|
-
// Both are read once per Host process (the card publishes them as plain view
|
|
9
|
-
// fields), so everything is cached and every failure degrades to '' / false —
|
|
10
|
-
// a version probe must never be able to break the plugin.
|
|
11
|
-
//
|
|
12
|
-
// Dependency direction: this module is a LEAF (node builtins only). lib/index.mjs
|
|
13
|
-
// imports it; nothing imports back, so no cycle can form.
|
|
14
|
-
|
|
15
|
-
import { readFileSync, statSync } from 'node:fs'
|
|
16
|
-
import { dirname, join, resolve } from 'node:path'
|
|
17
|
-
|
|
18
|
-
/** This plugin's npm package name (also the npm update-check target). */
|
|
19
|
-
export const PLUGIN_NAME = 'dsh-dbhub-live'
|
|
20
|
-
|
|
21
|
-
/** The first dsh release with the modern client UI contracts. */
|
|
22
|
-
export const DSH_MODERN_BASE = '0.2.1'
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Package names that identify the running dsh runtime when walking up from the
|
|
26
|
-
* process entry point. The CLI, the desktop runtime wrapper and the desktop app
|
|
27
|
-
* shell are all valid hosts.
|
|
28
|
-
*/
|
|
29
|
-
export const DSH_PACKAGE_NAMES = Object.freeze([
|
|
30
|
-
'@deepseek-ai/dsh',
|
|
31
|
-
'@deepseek-ai/dsh-desktop-runtime',
|
|
32
|
-
'@deepseek-ai/dsh-desktop',
|
|
33
|
-
])
|
|
34
|
-
|
|
35
|
-
// Accepts `v1.2.3`, `1.2.3-dev.1`, `1.2.3+build.5` and `1.2.3-rc.1+sha`.
|
|
36
|
-
const SEMVER_RE = /^[vV]?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/
|
|
37
|
-
|
|
38
|
-
const NUMERIC_RE = /^\d+$/
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Parse a semver string into comparable parts.
|
|
42
|
-
*
|
|
43
|
-
* Lenient on the leading `v` (release tags are usually written `v5.2.0`) and
|
|
44
|
-
* tolerant of any build metadata (which never participates in precedence), but
|
|
45
|
-
* strict about the core triplet and empty prerelease identifiers. Returns
|
|
46
|
-
* `undefined` for anything that is not semver — callers treat that as
|
|
47
|
-
* "unknown version", never as 0.0.0.
|
|
48
|
-
*
|
|
49
|
-
* @param {unknown} text
|
|
50
|
-
* @returns {{ major: number, minor: number, patch: number, prerelease: string[], raw: string } | undefined}
|
|
51
|
-
*/
|
|
52
|
-
export function parseSemver(text) {
|
|
53
|
-
if (typeof text !== 'string') return undefined
|
|
54
|
-
const raw = text.trim()
|
|
55
|
-
const match = SEMVER_RE.exec(raw)
|
|
56
|
-
if (!match) return undefined
|
|
57
|
-
const prerelease = match[4] ? match[4].split('.') : []
|
|
58
|
-
// "1.2.3-alpha..beta" is invalid semver: no identifier may be empty.
|
|
59
|
-
if (prerelease.length > 0 && prerelease.some((id) => id === '')) return undefined
|
|
60
|
-
return {
|
|
61
|
-
major: Number(match[1]),
|
|
62
|
-
minor: Number(match[2]),
|
|
63
|
-
patch: Number(match[3]),
|
|
64
|
-
prerelease,
|
|
65
|
-
raw,
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function compareTriplet(a, b) {
|
|
70
|
-
if (a.major !== b.major) return a.major < b.major ? -1 : 1
|
|
71
|
-
if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1
|
|
72
|
-
if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1
|
|
73
|
-
return 0
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Semver prerelease precedence (semver.org §11): a version WITHOUT prerelease
|
|
78
|
-
* outranks the same version WITH one; numeric identifiers compare numerically;
|
|
79
|
-
* numeric sorts below alphanumeric; alphanumeric compares in ASCII order; a
|
|
80
|
-
* longer identifier list wins when every shared identifier is equal.
|
|
81
|
-
*/
|
|
82
|
-
function comparePrerelease(a, b) {
|
|
83
|
-
if (a.length === 0 && b.length === 0) return 0
|
|
84
|
-
if (a.length === 0) return 1
|
|
85
|
-
if (b.length === 0) return -1
|
|
86
|
-
const shared = Math.min(a.length, b.length)
|
|
87
|
-
for (let i = 0; i < shared; i += 1) {
|
|
88
|
-
const left = a[i]
|
|
89
|
-
const right = b[i]
|
|
90
|
-
const leftNumeric = NUMERIC_RE.test(left)
|
|
91
|
-
const rightNumeric = NUMERIC_RE.test(right)
|
|
92
|
-
if (leftNumeric && rightNumeric) {
|
|
93
|
-
const l = Number(left)
|
|
94
|
-
const r = Number(right)
|
|
95
|
-
if (l !== r) return l < r ? -1 : 1
|
|
96
|
-
continue
|
|
97
|
-
}
|
|
98
|
-
// Numeric identifiers always have lower precedence than alphanumeric ones.
|
|
99
|
-
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1
|
|
100
|
-
if (left !== right) return left < right ? -1 : 1
|
|
101
|
-
}
|
|
102
|
-
if (a.length !== b.length) return a.length < b.length ? -1 : 1
|
|
103
|
-
return 0
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function compareParsed(a, b) {
|
|
107
|
-
const core = compareTriplet(a, b)
|
|
108
|
-
if (core !== 0) return core
|
|
109
|
-
return comparePrerelease(a.prerelease, b.prerelease)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Semver precedence between two versions (strings or already-parsed objects).
|
|
114
|
-
* Invalid input compares as "incomparable" and yields 0 — this must never throw.
|
|
115
|
-
*
|
|
116
|
-
* @returns {-1|0|1}
|
|
117
|
-
*/
|
|
118
|
-
export function compareSemver(a, b) {
|
|
119
|
-
const left = typeof a === 'object' && a !== null ? a : parseSemver(a)
|
|
120
|
-
const right = typeof b === 'object' && b !== null ? b : parseSemver(b)
|
|
121
|
-
if (!left || !right) return 0
|
|
122
|
-
return compareParsed(left, right)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* True when `candidate` is STRICTLY newer than `current` per semver precedence.
|
|
127
|
-
* Unknown on either side yields false: without a known current version an
|
|
128
|
-
* update cannot be claimed, so the card must not show an update banner.
|
|
129
|
-
*/
|
|
130
|
-
export function isNewer(candidate, current) {
|
|
131
|
-
const next = parseSemver(candidate)
|
|
132
|
-
const installed = parseSemver(current)
|
|
133
|
-
if (!next || !installed) return false
|
|
134
|
-
return compareParsed(next, installed) > 0
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* True when `version` has the same-or-newer numeric triplet than `base`
|
|
139
|
-
* (prerelease is ignored: `0.2.1-rc.2` counts as `0.2.1`). An unknown version is
|
|
140
|
-
* false — "cannot tell" must not be reported as "modern".
|
|
141
|
-
*/
|
|
142
|
-
export function atLeastBase(version, base = DSH_MODERN_BASE) {
|
|
143
|
-
const parsed = parseSemver(version)
|
|
144
|
-
const floor = parseSemver(base)
|
|
145
|
-
if (!parsed || !floor) return false
|
|
146
|
-
return compareTriplet(parsed, floor) >= 0
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
let cachedPluginVersion
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* This plugin's own version, read from the package.json next to lib/.
|
|
153
|
-
* Cached for the process lifetime (an installed package never changes under a
|
|
154
|
-
* running Host); '' when the file is missing or its version is not semver.
|
|
155
|
-
*/
|
|
156
|
-
export function readPluginVersion() {
|
|
157
|
-
if (cachedPluginVersion !== undefined) return cachedPluginVersion
|
|
158
|
-
cachedPluginVersion = ''
|
|
159
|
-
try {
|
|
160
|
-
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
161
|
-
const version = pkg && typeof pkg.version === 'string' ? pkg.version.trim() : ''
|
|
162
|
-
cachedPluginVersion = parseSemver(version) ? version : ''
|
|
163
|
-
} catch (e) {
|
|
164
|
-
cachedPluginVersion = ''
|
|
165
|
-
}
|
|
166
|
-
return cachedPluginVersion
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function isDirectory(abs) {
|
|
170
|
-
try {
|
|
171
|
-
return statSync(abs).isDirectory()
|
|
172
|
-
} catch (e) {
|
|
173
|
-
return false
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Walk up from the running process entry point looking for an installed dsh
|
|
179
|
-
* package.json. Exported so tests can drive it against a temp layout and a fake
|
|
180
|
-
* argv entry instead of touching the real installation.
|
|
181
|
-
*
|
|
182
|
-
* @param {string|undefined} entryPath - usually `process.argv[1]`
|
|
183
|
-
* @param {{ maxDepth?: number, packageNames?: ReadonlyArray<string> }} [options]
|
|
184
|
-
* @returns {string} the package version, or '' when nothing matched
|
|
185
|
-
*/
|
|
186
|
-
export function walkDshVersion(entryPath, options = {}) {
|
|
187
|
-
const maxDepth = Number.isInteger(options.maxDepth) && options.maxDepth > 0 ? options.maxDepth : 8
|
|
188
|
-
const names = new Set(
|
|
189
|
-
Array.isArray(options.packageNames) && options.packageNames.length > 0
|
|
190
|
-
? options.packageNames
|
|
191
|
-
: DSH_PACKAGE_NAMES,
|
|
192
|
-
)
|
|
193
|
-
if (typeof entryPath !== 'string' || entryPath.trim() === '') return ''
|
|
194
|
-
// `process.argv[1]` is normally a file, but a loader may hand over a directory
|
|
195
|
-
// or a path that does not exist yet; both resolve to a starting directory.
|
|
196
|
-
const abs = resolve(entryPath.trim())
|
|
197
|
-
let dir = isDirectory(abs) ? abs : dirname(abs)
|
|
198
|
-
for (let depth = 0; depth < maxDepth; depth += 1) {
|
|
199
|
-
try {
|
|
200
|
-
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
|
|
201
|
-
const version = pkg && typeof pkg.version === 'string' ? pkg.version.trim() : ''
|
|
202
|
-
if (pkg && typeof pkg.name === 'string' && names.has(pkg.name) && parseSemver(version)) {
|
|
203
|
-
return version
|
|
204
|
-
}
|
|
205
|
-
} catch (e) {
|
|
206
|
-
/* no package.json here (or unreadable/non-JSON): keep walking up */
|
|
207
|
-
}
|
|
208
|
-
const parent = dirname(dir)
|
|
209
|
-
if (!parent || parent === dir) break
|
|
210
|
-
dir = parent
|
|
211
|
-
}
|
|
212
|
-
return ''
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Best-effort identity of the running dsh runtime.
|
|
217
|
-
*
|
|
218
|
-
* `desktop` is true under the packaged desktop app (Electron main process) or
|
|
219
|
-
* whenever the desktop shell exports its own version through the environment.
|
|
220
|
-
* The version is resolved from the installed dsh package.json first (the
|
|
221
|
-
* authoritative value) and from the desktop env var second.
|
|
222
|
-
*
|
|
223
|
-
* @returns {{ version: string, desktop: boolean }}
|
|
224
|
-
*/
|
|
225
|
-
export function detectDshRuntime() {
|
|
226
|
-
const envVersion = process.env && typeof process.env.DSH_CLIENT_VERSION === 'string'
|
|
227
|
-
? process.env.DSH_CLIENT_VERSION.trim()
|
|
228
|
-
: ''
|
|
229
|
-
const desktop = Boolean(process.versions && process.versions.electron) || Boolean(envVersion)
|
|
230
|
-
let version = ''
|
|
231
|
-
try {
|
|
232
|
-
version = walkDshVersion(process.argv && process.argv[1], { maxDepth: 8 })
|
|
233
|
-
} catch (e) {
|
|
234
|
-
version = ''
|
|
235
|
-
}
|
|
236
|
-
if (!version && envVersion && parseSemver(envVersion)) version = envVersion
|
|
237
|
-
return { version, desktop }
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
* The modern-UI gate: a resolved dsh >= 0.2.1 is modern; an UNRESOLVABLE
|
|
242
|
-
* version is modern only on the desktop runtime (it ships the modern UI by
|
|
243
|
-
* definition, and its package layout is the least predictable one). An unknown
|
|
244
|
-
* version on web is treated as legacy, which is the safe assumption for the old
|
|
245
|
-
* 0.1.x line.
|
|
246
|
-
*/
|
|
247
|
-
export function dshIsModern(runtime) {
|
|
248
|
-
const info = runtime && typeof runtime === 'object' ? runtime : {}
|
|
249
|
-
const version = typeof info.version === 'string' ? info.version : ''
|
|
250
|
-
if (parseSemver(version)) return atLeastBase(version, DSH_MODERN_BASE)
|
|
251
|
-
return Boolean(info.desktop)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/**
|
|
255
|
-
* The version fields the Host publishes, computed once per Host core:
|
|
256
|
-
* `{ pluginVersion, dshVersion, dshModern, runtime }`. Plain strings/booleans —
|
|
257
|
-
* nothing here is secret and nothing here needs the network.
|
|
258
|
-
*/
|
|
259
|
-
export function versionMeta() {
|
|
260
|
-
const runtime = detectDshRuntime()
|
|
261
|
-
return {
|
|
262
|
-
pluginVersion: readPluginVersion(),
|
|
263
|
-
dshVersion: runtime.version,
|
|
264
|
-
dshModern: dshIsModern(runtime),
|
|
265
|
-
runtime: runtime.desktop ? 'desktop' : 'web',
|
|
266
|
-
}
|
|
267
|
-
}
|
|
1
|
+
// dsh-dbhub-live: version metadata for the Host.
|
|
2
|
+
//
|
|
3
|
+
// Two independent questions live here:
|
|
4
|
+
// 1. which version of THIS plugin is running (its own package.json), and
|
|
5
|
+
// 2. which dsh release is hosting it, and whether that release is new enough
|
|
6
|
+
// for the modern UI contracts (>= 0.2.1 base version).
|
|
7
|
+
//
|
|
8
|
+
// Both are read once per Host process (the card publishes them as plain view
|
|
9
|
+
// fields), so everything is cached and every failure degrades to '' / false —
|
|
10
|
+
// a version probe must never be able to break the plugin.
|
|
11
|
+
//
|
|
12
|
+
// Dependency direction: this module is a LEAF (node builtins only). lib/index.mjs
|
|
13
|
+
// imports it; nothing imports back, so no cycle can form.
|
|
14
|
+
|
|
15
|
+
import { readFileSync, statSync } from 'node:fs'
|
|
16
|
+
import { dirname, join, resolve } from 'node:path'
|
|
17
|
+
|
|
18
|
+
/** This plugin's npm package name (also the npm update-check target). */
|
|
19
|
+
export const PLUGIN_NAME = 'dsh-dbhub-live'
|
|
20
|
+
|
|
21
|
+
/** The first dsh release with the modern client UI contracts. */
|
|
22
|
+
export const DSH_MODERN_BASE = '0.2.1'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Package names that identify the running dsh runtime when walking up from the
|
|
26
|
+
* process entry point. The CLI, the desktop runtime wrapper and the desktop app
|
|
27
|
+
* shell are all valid hosts.
|
|
28
|
+
*/
|
|
29
|
+
export const DSH_PACKAGE_NAMES = Object.freeze([
|
|
30
|
+
'@deepseek-ai/dsh',
|
|
31
|
+
'@deepseek-ai/dsh-desktop-runtime',
|
|
32
|
+
'@deepseek-ai/dsh-desktop',
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
// Accepts `v1.2.3`, `1.2.3-dev.1`, `1.2.3+build.5` and `1.2.3-rc.1+sha`.
|
|
36
|
+
const SEMVER_RE = /^[vV]?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/
|
|
37
|
+
|
|
38
|
+
const NUMERIC_RE = /^\d+$/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse a semver string into comparable parts.
|
|
42
|
+
*
|
|
43
|
+
* Lenient on the leading `v` (release tags are usually written `v5.2.0`) and
|
|
44
|
+
* tolerant of any build metadata (which never participates in precedence), but
|
|
45
|
+
* strict about the core triplet and empty prerelease identifiers. Returns
|
|
46
|
+
* `undefined` for anything that is not semver — callers treat that as
|
|
47
|
+
* "unknown version", never as 0.0.0.
|
|
48
|
+
*
|
|
49
|
+
* @param {unknown} text
|
|
50
|
+
* @returns {{ major: number, minor: number, patch: number, prerelease: string[], raw: string } | undefined}
|
|
51
|
+
*/
|
|
52
|
+
export function parseSemver(text) {
|
|
53
|
+
if (typeof text !== 'string') return undefined
|
|
54
|
+
const raw = text.trim()
|
|
55
|
+
const match = SEMVER_RE.exec(raw)
|
|
56
|
+
if (!match) return undefined
|
|
57
|
+
const prerelease = match[4] ? match[4].split('.') : []
|
|
58
|
+
// "1.2.3-alpha..beta" is invalid semver: no identifier may be empty.
|
|
59
|
+
if (prerelease.length > 0 && prerelease.some((id) => id === '')) return undefined
|
|
60
|
+
return {
|
|
61
|
+
major: Number(match[1]),
|
|
62
|
+
minor: Number(match[2]),
|
|
63
|
+
patch: Number(match[3]),
|
|
64
|
+
prerelease,
|
|
65
|
+
raw,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function compareTriplet(a, b) {
|
|
70
|
+
if (a.major !== b.major) return a.major < b.major ? -1 : 1
|
|
71
|
+
if (a.minor !== b.minor) return a.minor < b.minor ? -1 : 1
|
|
72
|
+
if (a.patch !== b.patch) return a.patch < b.patch ? -1 : 1
|
|
73
|
+
return 0
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Semver prerelease precedence (semver.org §11): a version WITHOUT prerelease
|
|
78
|
+
* outranks the same version WITH one; numeric identifiers compare numerically;
|
|
79
|
+
* numeric sorts below alphanumeric; alphanumeric compares in ASCII order; a
|
|
80
|
+
* longer identifier list wins when every shared identifier is equal.
|
|
81
|
+
*/
|
|
82
|
+
function comparePrerelease(a, b) {
|
|
83
|
+
if (a.length === 0 && b.length === 0) return 0
|
|
84
|
+
if (a.length === 0) return 1
|
|
85
|
+
if (b.length === 0) return -1
|
|
86
|
+
const shared = Math.min(a.length, b.length)
|
|
87
|
+
for (let i = 0; i < shared; i += 1) {
|
|
88
|
+
const left = a[i]
|
|
89
|
+
const right = b[i]
|
|
90
|
+
const leftNumeric = NUMERIC_RE.test(left)
|
|
91
|
+
const rightNumeric = NUMERIC_RE.test(right)
|
|
92
|
+
if (leftNumeric && rightNumeric) {
|
|
93
|
+
const l = Number(left)
|
|
94
|
+
const r = Number(right)
|
|
95
|
+
if (l !== r) return l < r ? -1 : 1
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
// Numeric identifiers always have lower precedence than alphanumeric ones.
|
|
99
|
+
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1
|
|
100
|
+
if (left !== right) return left < right ? -1 : 1
|
|
101
|
+
}
|
|
102
|
+
if (a.length !== b.length) return a.length < b.length ? -1 : 1
|
|
103
|
+
return 0
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function compareParsed(a, b) {
|
|
107
|
+
const core = compareTriplet(a, b)
|
|
108
|
+
if (core !== 0) return core
|
|
109
|
+
return comparePrerelease(a.prerelease, b.prerelease)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Semver precedence between two versions (strings or already-parsed objects).
|
|
114
|
+
* Invalid input compares as "incomparable" and yields 0 — this must never throw.
|
|
115
|
+
*
|
|
116
|
+
* @returns {-1|0|1}
|
|
117
|
+
*/
|
|
118
|
+
export function compareSemver(a, b) {
|
|
119
|
+
const left = typeof a === 'object' && a !== null ? a : parseSemver(a)
|
|
120
|
+
const right = typeof b === 'object' && b !== null ? b : parseSemver(b)
|
|
121
|
+
if (!left || !right) return 0
|
|
122
|
+
return compareParsed(left, right)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* True when `candidate` is STRICTLY newer than `current` per semver precedence.
|
|
127
|
+
* Unknown on either side yields false: without a known current version an
|
|
128
|
+
* update cannot be claimed, so the card must not show an update banner.
|
|
129
|
+
*/
|
|
130
|
+
export function isNewer(candidate, current) {
|
|
131
|
+
const next = parseSemver(candidate)
|
|
132
|
+
const installed = parseSemver(current)
|
|
133
|
+
if (!next || !installed) return false
|
|
134
|
+
return compareParsed(next, installed) > 0
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* True when `version` has the same-or-newer numeric triplet than `base`
|
|
139
|
+
* (prerelease is ignored: `0.2.1-rc.2` counts as `0.2.1`). An unknown version is
|
|
140
|
+
* false — "cannot tell" must not be reported as "modern".
|
|
141
|
+
*/
|
|
142
|
+
export function atLeastBase(version, base = DSH_MODERN_BASE) {
|
|
143
|
+
const parsed = parseSemver(version)
|
|
144
|
+
const floor = parseSemver(base)
|
|
145
|
+
if (!parsed || !floor) return false
|
|
146
|
+
return compareTriplet(parsed, floor) >= 0
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let cachedPluginVersion
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* This plugin's own version, read from the package.json next to lib/.
|
|
153
|
+
* Cached for the process lifetime (an installed package never changes under a
|
|
154
|
+
* running Host); '' when the file is missing or its version is not semver.
|
|
155
|
+
*/
|
|
156
|
+
export function readPluginVersion() {
|
|
157
|
+
if (cachedPluginVersion !== undefined) return cachedPluginVersion
|
|
158
|
+
cachedPluginVersion = ''
|
|
159
|
+
try {
|
|
160
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
161
|
+
const version = pkg && typeof pkg.version === 'string' ? pkg.version.trim() : ''
|
|
162
|
+
cachedPluginVersion = parseSemver(version) ? version : ''
|
|
163
|
+
} catch (e) {
|
|
164
|
+
cachedPluginVersion = ''
|
|
165
|
+
}
|
|
166
|
+
return cachedPluginVersion
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isDirectory(abs) {
|
|
170
|
+
try {
|
|
171
|
+
return statSync(abs).isDirectory()
|
|
172
|
+
} catch (e) {
|
|
173
|
+
return false
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Walk up from the running process entry point looking for an installed dsh
|
|
179
|
+
* package.json. Exported so tests can drive it against a temp layout and a fake
|
|
180
|
+
* argv entry instead of touching the real installation.
|
|
181
|
+
*
|
|
182
|
+
* @param {string|undefined} entryPath - usually `process.argv[1]`
|
|
183
|
+
* @param {{ maxDepth?: number, packageNames?: ReadonlyArray<string> }} [options]
|
|
184
|
+
* @returns {string} the package version, or '' when nothing matched
|
|
185
|
+
*/
|
|
186
|
+
export function walkDshVersion(entryPath, options = {}) {
|
|
187
|
+
const maxDepth = Number.isInteger(options.maxDepth) && options.maxDepth > 0 ? options.maxDepth : 8
|
|
188
|
+
const names = new Set(
|
|
189
|
+
Array.isArray(options.packageNames) && options.packageNames.length > 0
|
|
190
|
+
? options.packageNames
|
|
191
|
+
: DSH_PACKAGE_NAMES,
|
|
192
|
+
)
|
|
193
|
+
if (typeof entryPath !== 'string' || entryPath.trim() === '') return ''
|
|
194
|
+
// `process.argv[1]` is normally a file, but a loader may hand over a directory
|
|
195
|
+
// or a path that does not exist yet; both resolve to a starting directory.
|
|
196
|
+
const abs = resolve(entryPath.trim())
|
|
197
|
+
let dir = isDirectory(abs) ? abs : dirname(abs)
|
|
198
|
+
for (let depth = 0; depth < maxDepth; depth += 1) {
|
|
199
|
+
try {
|
|
200
|
+
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
|
|
201
|
+
const version = pkg && typeof pkg.version === 'string' ? pkg.version.trim() : ''
|
|
202
|
+
if (pkg && typeof pkg.name === 'string' && names.has(pkg.name) && parseSemver(version)) {
|
|
203
|
+
return version
|
|
204
|
+
}
|
|
205
|
+
} catch (e) {
|
|
206
|
+
/* no package.json here (or unreadable/non-JSON): keep walking up */
|
|
207
|
+
}
|
|
208
|
+
const parent = dirname(dir)
|
|
209
|
+
if (!parent || parent === dir) break
|
|
210
|
+
dir = parent
|
|
211
|
+
}
|
|
212
|
+
return ''
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Best-effort identity of the running dsh runtime.
|
|
217
|
+
*
|
|
218
|
+
* `desktop` is true under the packaged desktop app (Electron main process) or
|
|
219
|
+
* whenever the desktop shell exports its own version through the environment.
|
|
220
|
+
* The version is resolved from the installed dsh package.json first (the
|
|
221
|
+
* authoritative value) and from the desktop env var second.
|
|
222
|
+
*
|
|
223
|
+
* @returns {{ version: string, desktop: boolean }}
|
|
224
|
+
*/
|
|
225
|
+
export function detectDshRuntime() {
|
|
226
|
+
const envVersion = process.env && typeof process.env.DSH_CLIENT_VERSION === 'string'
|
|
227
|
+
? process.env.DSH_CLIENT_VERSION.trim()
|
|
228
|
+
: ''
|
|
229
|
+
const desktop = Boolean(process.versions && process.versions.electron) || Boolean(envVersion)
|
|
230
|
+
let version = ''
|
|
231
|
+
try {
|
|
232
|
+
version = walkDshVersion(process.argv && process.argv[1], { maxDepth: 8 })
|
|
233
|
+
} catch (e) {
|
|
234
|
+
version = ''
|
|
235
|
+
}
|
|
236
|
+
if (!version && envVersion && parseSemver(envVersion)) version = envVersion
|
|
237
|
+
return { version, desktop }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The modern-UI gate: a resolved dsh >= 0.2.1 is modern; an UNRESOLVABLE
|
|
242
|
+
* version is modern only on the desktop runtime (it ships the modern UI by
|
|
243
|
+
* definition, and its package layout is the least predictable one). An unknown
|
|
244
|
+
* version on web is treated as legacy, which is the safe assumption for the old
|
|
245
|
+
* 0.1.x line.
|
|
246
|
+
*/
|
|
247
|
+
export function dshIsModern(runtime) {
|
|
248
|
+
const info = runtime && typeof runtime === 'object' ? runtime : {}
|
|
249
|
+
const version = typeof info.version === 'string' ? info.version : ''
|
|
250
|
+
if (parseSemver(version)) return atLeastBase(version, DSH_MODERN_BASE)
|
|
251
|
+
return Boolean(info.desktop)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The version fields the Host publishes, computed once per Host core:
|
|
256
|
+
* `{ pluginVersion, dshVersion, dshModern, runtime }`. Plain strings/booleans —
|
|
257
|
+
* nothing here is secret and nothing here needs the network.
|
|
258
|
+
*/
|
|
259
|
+
export function versionMeta() {
|
|
260
|
+
const runtime = detectDshRuntime()
|
|
261
|
+
return {
|
|
262
|
+
pluginVersion: readPluginVersion(),
|
|
263
|
+
dshVersion: runtime.version,
|
|
264
|
+
dshModern: dshIsModern(runtime),
|
|
265
|
+
runtime: runtime.desktop ? 'desktop' : 'web',
|
|
266
|
+
}
|
|
267
|
+
}
|