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.
@@ -1,17 +1,24 @@
1
1
  /**
2
- * Update handling — Windows auto-update, macOS manual download.
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 / dev mode: falls back to a manual check that opens the GitHub
9
- * Releases page (macOS auto-update needs a Developer ID signature which
10
- * this project does not have yet).
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 { dirname, join } from 'node:path'
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 PKG_VERSION = (() => {
35
+ const PKG_INFO = (() => {
29
36
  try {
30
- return JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8')).version
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: isPlugin
140
- ? '插件形态请到 DSH 网页的「设置 插件市场 已安装」里点「更新」,完成后按提示重启即可生效。'
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) openUrl(isPlugin ? loadConfig().targetUrl : r.url)
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.8",
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.8
1
+ 0.1.9