dsh-clean-desktop-shell 0.1.8 → 0.1.9
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/electron/update.js +198 -14
- package/package.json +1 -1
- package/version.txt +1 -1
package/electron/update.js
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Update handling —
|
|
2
|
+
* Update handling — three install forms, three update sources.
|
|
3
3
|
*
|
|
4
4
|
* Windows (packaged app): uses electron-updater to download the new
|
|
5
5
|
* installer from GitHub Releases in the background and install it on
|
|
6
6
|
* restart. Progress is shown in the small progress window.
|
|
7
7
|
*
|
|
8
|
-
* macOS
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* macOS (packaged app): manual check that opens the GitHub Releases page
|
|
9
|
+
* (macOS auto-update needs a Developer ID signature which this project
|
|
10
|
+
* does not have yet).
|
|
11
|
+
*
|
|
12
|
+
* Plugin mode (npm-installed, !app.isPackaged): checks the npm registry for
|
|
13
|
+
* dist-tags.latest and offers a one-click update executed through the
|
|
14
|
+
* package manager that actually installed the plugin (inferred from the
|
|
15
|
+
* lockfile next to the install, vercel-style), with command variants to
|
|
16
|
+
* absorb environment and pnpm-version differences.
|
|
11
17
|
*/
|
|
12
18
|
import { app, shell, dialog } from 'electron'
|
|
13
|
-
import { readFileSync } from 'node:fs'
|
|
14
|
-
import {
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
20
|
+
import { spawn } from 'node:child_process'
|
|
21
|
+
import { basename, dirname, join } from 'node:path'
|
|
15
22
|
import { fileURLToPath } from 'node:url'
|
|
16
23
|
import { showProgress, setProgress, closeProgress } from './progress.js'
|
|
17
24
|
import { loadConfig } from './config.js'
|
|
@@ -25,13 +32,16 @@ const NPM_REGISTRY_API = 'https://registry.npmjs.org/dsh-clean-desktop-shell'
|
|
|
25
32
|
// the Electron runtime version (e.g. 33.4.11). Read the plugin's own version
|
|
26
33
|
// from its package.json instead.
|
|
27
34
|
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
28
|
-
const
|
|
35
|
+
const PKG_INFO = (() => {
|
|
29
36
|
try {
|
|
30
|
-
return JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'))
|
|
37
|
+
return JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'))
|
|
31
38
|
} catch {
|
|
32
39
|
return null
|
|
33
40
|
}
|
|
34
41
|
})()
|
|
42
|
+
const PKG_VERSION = PKG_INFO?.version ?? null
|
|
43
|
+
// Read from the plugin's own manifest — never hardcode the package name.
|
|
44
|
+
const PKG_NAME = PKG_INFO?.name ?? 'dsh-clean-desktop-shell'
|
|
35
45
|
|
|
36
46
|
let autoUpdater = null
|
|
37
47
|
let updaterPromise = null
|
|
@@ -132,18 +142,44 @@ export async function checkForUpdatesAuto() {
|
|
|
132
142
|
const isPlugin = !app.isPackaged
|
|
133
143
|
const r = isPlugin ? await checkForUpdateNpm() : await checkForUpdate()
|
|
134
144
|
if (r.hasUpdate) {
|
|
145
|
+
if (!isPlugin) {
|
|
146
|
+
const choice = dialog.showMessageBoxSync({
|
|
147
|
+
type: 'info',
|
|
148
|
+
title: '发现新版本',
|
|
149
|
+
message: `当前版本 ${r.current},最新版本 ${r.latest}。`,
|
|
150
|
+
detail: 'macOS 自动更新需要代码签名,当前请前往 GitHub Releases 手动下载。',
|
|
151
|
+
buttons: ['前往下载', '取消'],
|
|
152
|
+
defaultId: 0,
|
|
153
|
+
cancelId: 1,
|
|
154
|
+
})
|
|
155
|
+
if (choice === 0) openUrl(r.url)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Plugin mode (Plan B + A): offer a one-click update executed through the
|
|
160
|
+
// package manager that actually installed this package, and show the
|
|
161
|
+
// equivalent manual command plus the DSH market path.
|
|
162
|
+
const plan = detectPluginUpdatePlan()
|
|
163
|
+
const variants = plan ? updateCommandVariants(plan.pm) : []
|
|
164
|
+
const cmdLine = variants[0] || null
|
|
165
|
+
const marketHint = '① DSH 网页「设置 → 插件市场 → 已安装」→「更新」(完成后按提示重启)。'
|
|
166
|
+
const detail = cmdLine
|
|
167
|
+
? `${marketHint}\n② 或在目录 ${plan.profileRoot} 下执行:\n${cmdLine}\n(按 Ctrl+C 可复制本对话框全部文字)`
|
|
168
|
+
: marketHint
|
|
135
169
|
const choice = dialog.showMessageBoxSync({
|
|
136
170
|
type: 'info',
|
|
137
171
|
title: '发现新版本',
|
|
138
172
|
message: `当前版本 ${r.current},最新版本 ${r.latest}。`,
|
|
139
|
-
detail
|
|
140
|
-
|
|
141
|
-
: 'macOS 自动更新需要代码签名,当前请前往 GitHub Releases 手动下载。',
|
|
142
|
-
buttons: isPlugin ? ['打开 DSH 网页', '稍后'] : ['前往下载', '取消'],
|
|
173
|
+
detail,
|
|
174
|
+
buttons: cmdLine ? ['立即更新', '打开 DSH 网页', '稍后'] : ['打开 DSH 网页', '稍后'],
|
|
143
175
|
defaultId: 0,
|
|
144
|
-
cancelId: 1,
|
|
176
|
+
cancelId: cmdLine ? 2 : 1,
|
|
145
177
|
})
|
|
146
|
-
if (choice === 0)
|
|
178
|
+
if (cmdLine && choice === 0) {
|
|
179
|
+
await updatePluginViaPackageManager(plan, variants, r)
|
|
180
|
+
} else if (choice === (cmdLine ? 1 : 0)) {
|
|
181
|
+
openUrl(loadConfig().targetUrl)
|
|
182
|
+
}
|
|
147
183
|
} else if (r.latest) {
|
|
148
184
|
dialog.showMessageBoxSync({
|
|
149
185
|
type: 'info',
|
|
@@ -266,3 +302,151 @@ export async function checkForUpdateNpm() {
|
|
|
266
302
|
url,
|
|
267
303
|
}
|
|
268
304
|
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Locate the directory whose package.json declares this plugin as a dependency
|
|
308
|
+
* (the DSH profile root: <root>/node_modules/<pkg>), and infer the package
|
|
309
|
+
* manager that manages it from the lockfile found there (vercel-style).
|
|
310
|
+
* @returns {{ pm: 'pnpm'|'npm'|'yarn'|'bun', profileRoot: string } | null}
|
|
311
|
+
*/
|
|
312
|
+
function detectPluginUpdatePlan() {
|
|
313
|
+
try {
|
|
314
|
+
const nm = dirname(PKG_ROOT)
|
|
315
|
+
if (basename(nm) !== 'node_modules') return null
|
|
316
|
+
const profileRoot = dirname(nm)
|
|
317
|
+
if (!existsSync(join(profileRoot, 'package.json'))) return null
|
|
318
|
+
const pm = existsSync(join(profileRoot, 'pnpm-lock.yaml')) ? 'pnpm'
|
|
319
|
+
: existsSync(join(profileRoot, 'package-lock.json')) ? 'npm'
|
|
320
|
+
: existsSync(join(profileRoot, 'yarn.lock')) ? 'yarn'
|
|
321
|
+
: (existsSync(join(profileRoot, 'bun.lockb')) || existsSync(join(profileRoot, 'bun.lock'))) ? 'bun'
|
|
322
|
+
: null
|
|
323
|
+
return pm ? { pm, profileRoot } : null
|
|
324
|
+
} catch {
|
|
325
|
+
return null
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Command candidates for the detected package manager, ordered from the most
|
|
331
|
+
* explicit to the most tolerant. Environment differences (pnpm only reachable
|
|
332
|
+
* via corepack) and pnpm version/flag differences are absorbed by trying them
|
|
333
|
+
* in order — the first variant that exits 0 wins. Quoting via JSON.stringify
|
|
334
|
+
* keeps paths/args safe in cmd.exe, PowerShell and POSIX shells alike.
|
|
335
|
+
*/
|
|
336
|
+
function updateCommandVariants(pm) {
|
|
337
|
+
const q = JSON.stringify(PKG_NAME)
|
|
338
|
+
switch (pm) {
|
|
339
|
+
case 'pnpm':
|
|
340
|
+
return [
|
|
341
|
+
`pnpm update ${q} --latest --config.minimumReleaseAge=0`,
|
|
342
|
+
`pnpm update ${q} --latest`,
|
|
343
|
+
`pnpm update ${q}`,
|
|
344
|
+
`corepack pnpm update ${q} --latest`,
|
|
345
|
+
]
|
|
346
|
+
case 'npm':
|
|
347
|
+
return [`npm install ${q}@latest`]
|
|
348
|
+
case 'yarn':
|
|
349
|
+
return [`yarn up ${q}@latest`, `yarn upgrade ${q} --latest`]
|
|
350
|
+
case 'bun':
|
|
351
|
+
return [`bun update ${q}`, `bun add ${q}@latest`]
|
|
352
|
+
default:
|
|
353
|
+
return []
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Run a shell command, capturing tail output. Node's spawn with shell:true
|
|
359
|
+
* always uses cmd.exe on Windows (regardless of the user's login shell being
|
|
360
|
+
* PowerShell or cmd), which is also required for .cmd shims like pnpm — a
|
|
361
|
+
* bare spawn would throw EINVAL. Resolves { ok, out, err }.
|
|
362
|
+
*/
|
|
363
|
+
function runShell(cmd, cwd, timeoutMs = 5 * 60 * 1000) {
|
|
364
|
+
return new Promise((resolve) => {
|
|
365
|
+
let out = ''
|
|
366
|
+
let err = ''
|
|
367
|
+
let settled = false
|
|
368
|
+
const child = spawn(cmd, {
|
|
369
|
+
cwd,
|
|
370
|
+
shell: true,
|
|
371
|
+
windowsHide: true, // no console flash from the GUI process
|
|
372
|
+
})
|
|
373
|
+
const timer = setTimeout(() => {
|
|
374
|
+
if (!settled) child.kill()
|
|
375
|
+
}, timeoutMs)
|
|
376
|
+
child.stdout?.on('data', (d) => { out += d })
|
|
377
|
+
child.stderr?.on('data', (d) => { err += d })
|
|
378
|
+
child.on('error', () => {
|
|
379
|
+
settled = true
|
|
380
|
+
clearTimeout(timer)
|
|
381
|
+
resolve({ ok: false, out: tail(out), err: tail(err) })
|
|
382
|
+
})
|
|
383
|
+
child.on('close', (code) => {
|
|
384
|
+
if (settled) return
|
|
385
|
+
settled = true
|
|
386
|
+
clearTimeout(timer)
|
|
387
|
+
resolve({ ok: code === 0, out: tail(out), err: tail(err) })
|
|
388
|
+
})
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function tail(s, n = 4000) {
|
|
393
|
+
return s.length > n ? s.slice(-n) : s
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Read the currently installed plugin version from disk (post-update check). */
|
|
397
|
+
function installedPluginVersion() {
|
|
398
|
+
try {
|
|
399
|
+
return JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8')).version || null
|
|
400
|
+
} catch {
|
|
401
|
+
return null
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* One-click plugin update: try each command variant until one exits 0, then
|
|
407
|
+
* verify the on-disk version actually changed. Three outcomes — success,
|
|
408
|
+
* "ran but version unchanged" (constraint/cooldown), or failure with the
|
|
409
|
+
* attempted commands and output tail for manual retry.
|
|
410
|
+
*/
|
|
411
|
+
async function updatePluginViaPackageManager(plan, variants, check) {
|
|
412
|
+
showProgress({ title: '插件更新', message: `正在通过 ${plan.pm} 更新插件…` })
|
|
413
|
+
let result = null
|
|
414
|
+
for (const cmd of variants) {
|
|
415
|
+
setProgress({ title: '插件更新', message: `正在执行:${cmd}`, state: 'busy' })
|
|
416
|
+
result = await runShell(cmd, plan.profileRoot)
|
|
417
|
+
if (result.ok) break
|
|
418
|
+
}
|
|
419
|
+
closeProgress()
|
|
420
|
+
|
|
421
|
+
const newVer = installedPluginVersion()
|
|
422
|
+
if (result?.ok && newVer && newVer !== check.current) {
|
|
423
|
+
dialog.showMessageBoxSync({
|
|
424
|
+
type: 'info',
|
|
425
|
+
title: '更新完成',
|
|
426
|
+
message: `插件已更新到 ${newVer}。`,
|
|
427
|
+
detail: '重启 dsh web 后生效(可从本应用托盘菜单重启后端)。',
|
|
428
|
+
})
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
if (result?.ok) {
|
|
432
|
+
dialog.showMessageBoxSync({
|
|
433
|
+
type: 'warning',
|
|
434
|
+
title: '版本未变化',
|
|
435
|
+
message: `命令执行成功,但插件版本仍为 ${check.current}。`,
|
|
436
|
+
detail: '可能被 package.json 的版本约束或新版本冷却策略限制,可稍后重试,或到 DSH 插件市场更新。',
|
|
437
|
+
})
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
const logTail = [result?.out, result?.err].filter(Boolean).join('\n').trim()
|
|
441
|
+
dialog.showMessageBoxSync({
|
|
442
|
+
type: 'warning',
|
|
443
|
+
title: '自动更新失败',
|
|
444
|
+
message: '无法自动更新插件,请手动执行或到 DSH 插件市场更新。',
|
|
445
|
+
detail: [
|
|
446
|
+
`已依次尝试:\n${variants.join('\n')}`,
|
|
447
|
+
logTail ? `命令输出(末尾):\n${logTail}` : '',
|
|
448
|
+
`手动命令(在目录 ${plan.profileRoot} 下):\n${variants[0]}`,
|
|
449
|
+
'按 Ctrl+C 可复制本对话框全部文字。',
|
|
450
|
+
].filter(Boolean).join('\n\n'),
|
|
451
|
+
})
|
|
452
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-clean-desktop-shell",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Clean desktop shell for DeepSeek Harness (DSH) as a DSH plugin — wraps your web profile in a native window, tray-managed backend, offline auto-reconnect, zero visual changes. DSH 插件形态的纯净桌面壳:复用现有 web profile,托盘管理后端,零视觉改造。",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
package/version.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.9
|