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/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
+ }