zen-gitsync 2.16.42 → 2.16.43

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.
Files changed (60) hide show
  1. package/package.json +1 -1
  2. package/src/ui/public/assets/AgentView-CVfH_WTL.js +4 -0
  3. package/src/ui/public/assets/AppVersionBadge-BKQZrA46.js +2 -0
  4. package/src/ui/public/assets/BranchSelector-BrmzYPb2.js +1 -0
  5. package/src/ui/public/assets/CommitForm-CSAwdqxy.js +6 -0
  6. package/src/ui/public/assets/CommonDialog-ChSKsEz2.css +1 -0
  7. package/src/ui/public/assets/CommonDialog-Clc3mo5h.js +1 -0
  8. package/src/ui/public/assets/EditorView-B05M4l8X.js +1 -0
  9. package/src/ui/public/assets/FlowExecutionViewer-Du0JPzV8.js +1 -0
  10. package/src/ui/public/assets/FlowOrchestrationWorkspace-Kg7HaggD.js +21 -0
  11. package/src/ui/public/assets/LogList-DSl8gjqq.js +7 -0
  12. package/src/ui/public/assets/MindmapView-KeS9uPEF.js +2 -0
  13. package/src/ui/public/assets/MonitorView-BYqPPmpL.js +1 -0
  14. package/src/ui/public/assets/ProjectStartupButton-COEXIVyh.js +1 -0
  15. package/src/ui/public/assets/{RemoteRepoCard-DFWs9d6t.js → RemoteRepoCard-Cqyksiy4.js} +2 -2
  16. package/src/ui/public/assets/SourceMapView-DhqMQTQD.js +3 -0
  17. package/src/ui/public/assets/SvgIcon-CDNsk4-Q.css +1 -0
  18. package/src/ui/public/assets/SvgIcon-DLr4V32f.js +1 -0
  19. package/src/ui/public/assets/UserInputNode-Do__vhj9.js +1 -0
  20. package/src/ui/public/assets/WorkbenchView-Dk5OlqZz.js +28 -0
  21. package/src/ui/public/assets/_plugin-vue_export-helper-CMAHJXhc.js +17 -0
  22. package/src/ui/public/assets/configStore-D666qKi0.js +1 -0
  23. package/src/ui/public/assets/element-plus-D3Dn7nX0.css +1 -0
  24. package/src/ui/public/assets/element-plus-DXuL5yWp.js +39 -0
  25. package/src/ui/public/assets/{flow-mindmap-BqudHiZw.js → flow-mindmap-Bi227PSm.js} +1 -1
  26. package/src/ui/public/assets/index-BT90zGWp.js +89 -0
  27. package/src/ui/public/assets/index-RKGE_YSG.css +1 -0
  28. package/src/ui/public/assets/{monaco-DPQ3OaiL.js → monaco-43aputPh.js} +2 -2
  29. package/src/ui/public/assets/{office-docx-DOvu6SBd.js → office-docx-ZOPof2Jf.js} +12 -12
  30. package/src/ui/public/assets/{office-excel-oizyPOWk.js → office-excel-DxreApmb.js} +2 -2
  31. package/src/ui/public/assets/{office-pptx-CCaqOF63.js → office-pptx-CR0bf9zY.js} +1 -1
  32. package/src/ui/public/assets/socket-io-BZIzo1US.js +1 -0
  33. package/src/ui/public/assets/toolsStore-DsvD8S_8.js +1 -0
  34. package/src/ui/public/assets/{ts.worker-METxwbDZ.js → ts.worker-Dth06zuC.js} +16 -1
  35. package/src/ui/public/assets/{vendor-CD2SBxQj.js → vendor-I3yQCqJK.js} +38 -38
  36. package/src/ui/public/assets/{vue-flow-BEmfD-aa.js → vue-flow-DV5FYL9P.js} +2 -2
  37. package/src/ui/public/index.html +16 -9
  38. package/src/ui/server/routes/fileOpen.js +248 -20
  39. package/src/ui/server/routes/fileOpen.test.js +239 -0
  40. package/src/ui/server/routes/workbench/agentSessionStore.js +173 -173
  41. package/src/ui/public/assets/AgentView-DjZTGHd_.js +0 -4
  42. package/src/ui/public/assets/AppVersionBadge-jSrdjNi3.js +0 -2
  43. package/src/ui/public/assets/BranchSelector-CNHhFgaH.js +0 -1
  44. package/src/ui/public/assets/CommitForm-B4rwEBIH.js +0 -6
  45. package/src/ui/public/assets/EditorView-D8Zdu0id.js +0 -1
  46. package/src/ui/public/assets/FlowExecutionViewer-Ci56tI0_.js +0 -1
  47. package/src/ui/public/assets/FlowOrchestrationWorkspace-WqB1AJl6.js +0 -21
  48. package/src/ui/public/assets/LogList-DkwwTvhf.js +0 -7
  49. package/src/ui/public/assets/MindmapView-CX1qlHl3.js +0 -2
  50. package/src/ui/public/assets/MonitorView-fnUCn0oW.js +0 -1
  51. package/src/ui/public/assets/ProjectStartupButton-DJZiuAuR.js +0 -1
  52. package/src/ui/public/assets/SourceMapView-mCG7TN0y.js +0 -3
  53. package/src/ui/public/assets/UserInputNode-C6pUnTWe.js +0 -1
  54. package/src/ui/public/assets/WorkbenchView-DgeUjdHD.js +0 -28
  55. package/src/ui/public/assets/element-plus-CXfbWXaJ.css +0 -1
  56. package/src/ui/public/assets/element-plus-DqggPOBO.js +0 -47
  57. package/src/ui/public/assets/index-B2a65GAv.css +0 -1
  58. package/src/ui/public/assets/index-DAgSqrPI.js +0 -104
  59. package/src/ui/public/assets/socket-io-B9UuzwTN.js +0 -1
  60. /package/src/ui/public/assets/{dagre-BCXBo6I9.js → dagre-97WoLjFt.js} +0 -0
@@ -10,20 +10,27 @@
10
10
  <link rel="preconnect" href="https://fonts.googleapis.com" />
11
11
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
12
12
  <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,400&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
13
- <script type="module" crossorigin src="/assets/index-DAgSqrPI.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-BT90zGWp.js"></script>
14
14
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-BM3Ffeng.js">
15
- <link rel="modulepreload" crossorigin href="/assets/monaco-DPQ3OaiL.js">
16
- <link rel="modulepreload" crossorigin href="/assets/element-plus-DqggPOBO.js">
17
- <link rel="modulepreload" crossorigin href="/assets/vendor-CD2SBxQj.js">
18
- <link rel="modulepreload" crossorigin href="/assets/socket-io-B9UuzwTN.js">
19
- <link rel="modulepreload" crossorigin href="/assets/vue-flow-BEmfD-aa.js">
20
- <link rel="modulepreload" crossorigin href="/assets/flow-mindmap-BqudHiZw.js">
15
+ <link rel="modulepreload" crossorigin href="/assets/monaco-43aputPh.js">
16
+ <link rel="modulepreload" crossorigin href="/assets/element-plus-DXuL5yWp.js">
17
+ <link rel="modulepreload" crossorigin href="/assets/vendor-I3yQCqJK.js">
18
+ <link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-CMAHJXhc.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/socket-io-BZIzo1US.js">
20
+ <link rel="modulepreload" crossorigin href="/assets/vue-flow-DV5FYL9P.js">
21
+ <link rel="modulepreload" crossorigin href="/assets/flow-mindmap-Bi227PSm.js">
22
+ <link rel="modulepreload" crossorigin href="/assets/SvgIcon-DLr4V32f.js">
23
+ <link rel="modulepreload" crossorigin href="/assets/CommonDialog-Clc3mo5h.js">
24
+ <link rel="modulepreload" crossorigin href="/assets/configStore-D666qKi0.js">
25
+ <link rel="modulepreload" crossorigin href="/assets/toolsStore-DsvD8S_8.js">
21
26
  <link rel="stylesheet" crossorigin href="/assets/monaco-B2lPvkrc.css">
22
- <link rel="stylesheet" crossorigin href="/assets/element-plus-CXfbWXaJ.css">
27
+ <link rel="stylesheet" crossorigin href="/assets/element-plus-D3Dn7nX0.css">
23
28
  <link rel="stylesheet" crossorigin href="/assets/vendor-BkgN6wPK.css">
24
29
  <link rel="stylesheet" crossorigin href="/assets/vue-flow-CavUTEzp.css">
25
30
  <link rel="stylesheet" crossorigin href="/assets/flow-mindmap-DnzM9SlK.css">
26
- <link rel="stylesheet" crossorigin href="/assets/index-B2a65GAv.css">
31
+ <link rel="stylesheet" crossorigin href="/assets/SvgIcon-CDNsk4-Q.css">
32
+ <link rel="stylesheet" crossorigin href="/assets/CommonDialog-ChSKsEz2.css">
33
+ <link rel="stylesheet" crossorigin href="/assets/index-RKGE_YSG.css">
27
34
  </head>
28
35
  <body>
29
36
  <div id="app"></div>
@@ -15,6 +15,7 @@
15
15
  import fs from 'fs/promises';
16
16
  import { asyncRoute, HttpError } from '../utils/asyncRoute.js';
17
17
  import path from 'path';
18
+ import os from 'os';
18
19
  import open from 'open';
19
20
  import { spawn, spawnSync } from 'child_process';
20
21
 
@@ -44,6 +45,61 @@ function commandExists(command, platform = process.platform) {
44
45
  return result.status === 0;
45
46
  }
46
47
 
48
+ /**
49
+ * 从 `--version` 的输出里提取 semver 版本号。
50
+ * 各工具输出格式五花八门:
51
+ * dsh → "0.1.1-rc.2"
52
+ * claude → "1.0.60 (Claude Code)"
53
+ * code → "1.92.0 abc123def (commit ...)"
54
+ * opencode → "opencode 0.1.30"
55
+ * 策略:取第一个 semver 模式(含 prerelease 0.1.1-rc.2 / build 1.0.0+build.1)。
56
+ * 提不出来返回 null(调用方按"未知版本"处理,不影响安装判定)。
57
+ */
58
+ export function parseVersionOutput(text) {
59
+ if (!text) return null;
60
+ const match = String(text).match(/\d+\.\d+\.\d+(?:[-+][\w.-]+)?/);
61
+ return match ? match[0] : null;
62
+ }
63
+
64
+ // ── npm registry 最新版本查询(右键更新菜单的「当前 → 最新」提示用)──────────
65
+ // 为什么走 HTTP 而不是 `npm view <pkg> version`:spawnSync('npm.cmd', ...) 在
66
+ // server 子进程里不稳(prefix -g 曾返回 "…npm:3" 废地址,见 findDshExecutable
67
+ // 的修复记录),HTTP 直查又快又可控。
68
+ // registry 优先级:~/.npmrc 的 registry 字段(用户可能配了镜像)→ npmmirror 默认。
69
+
70
+ let cachedRegistryBase = null;
71
+
72
+ export function getRegistryBase(readFile = fs.readFile) {
73
+ if (cachedRegistryBase) return cachedRegistryBase;
74
+ return readFile(path.join(os.homedir(), '.npmrc'), 'utf8').then((npmrc) => {
75
+ const match = String(npmrc).match(/^\s*registry\s*=\s*(\S+)/m);
76
+ cachedRegistryBase = match
77
+ ? match[1].replace(/\/+$/, '')
78
+ : 'https://registry.npmmirror.com';
79
+ return cachedRegistryBase;
80
+ }).catch(() => (cachedRegistryBase = 'https://registry.npmmirror.com'));
81
+ }
82
+
83
+ export async function fetchLatestVersion(packageName, { timeoutMs = 8000, fetchImpl = fetch } = {}) {
84
+ if (!packageName || !/^[\w@/.-]+$/.test(packageName)) return null;
85
+ const base = await getRegistryBase();
86
+ const controller = new AbortController();
87
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
88
+ try {
89
+ const resp = await fetchImpl(`${base}/${packageName}/latest`, {
90
+ signal: controller.signal,
91
+ headers: { accept: 'application/json' },
92
+ });
93
+ if (!resp.ok) return null;
94
+ const data = await resp.json();
95
+ return typeof data?.version === 'string' ? data.version : null;
96
+ } catch {
97
+ return null; // 网络失败/超时:菜单显示原文案,不影响任何功能
98
+ } finally {
99
+ clearTimeout(timer);
100
+ }
101
+ }
102
+
47
103
  /**
48
104
  * 返回当前平台可展示、可执行的安装方案。
49
105
  * executable/args 只在服务端使用;前端只能提交固定 tool id,不能提交命令。
@@ -64,6 +120,12 @@ export function getToolInstallers(platform = process.platform, hasCommand = comm
64
120
  : '未检测到 npm,请先安装 Node.js/npm,或按照官方文档手动安装。',
65
121
  executable: npmExecutable,
66
122
  args: ['install', '-g', packageName],
123
+ // 升级变体:@latest 强制同步到最新版。已是最新时 npm 自然跳过。
124
+ updateCommand: `npm install -g ${packageName}@latest`,
125
+ updatePackageManager: 'npm',
126
+ updateNote: '将在新终端中执行 `npm install -g <pkg>@latest`。已是最新时 npm 会跳过。',
127
+ updateExecutable: npmExecutable,
128
+ updateArgs: ['install', '-g', `${packageName}@latest`],
67
129
  };
68
130
  }
69
131
 
@@ -79,6 +141,18 @@ export function getToolInstallers(platform = process.platform, hasCommand = comm
79
141
  : '未检测到 winget,请使用 VS Code 官方安装程序。',
80
142
  executable: 'winget',
81
143
  args: ['install', '--id', 'Microsoft.VisualStudioCode', '-e', '--accept-package-agreements', '--accept-source-agreements'],
144
+ // winget 的升级动词是 upgrade(不是 install)
145
+ updateCommand: wingetAvailable
146
+ ? 'winget upgrade --id Microsoft.VisualStudioCode -e --accept-package-agreements --accept-source-agreements'
147
+ : undefined,
148
+ updatePackageManager: 'winget',
149
+ updateNote: wingetAvailable
150
+ ? '将在新终端中通过 winget upgrade 升级 VS Code。'
151
+ : '未检测到 winget,请使用 VS Code 官方安装包。',
152
+ updateExecutable: wingetAvailable ? 'winget' : undefined,
153
+ updateArgs: wingetAvailable
154
+ ? ['upgrade', '--id', 'Microsoft.VisualStudioCode', '-e', '--accept-package-agreements', '--accept-source-agreements']
155
+ : undefined,
82
156
  };
83
157
  } else if (platform === 'darwin') {
84
158
  const brewAvailable = hasCommand('brew', platform);
@@ -92,6 +166,11 @@ export function getToolInstallers(platform = process.platform, hasCommand = comm
92
166
  : '未检测到 Homebrew,请使用 VS Code 官方安装程序。',
93
167
  executable: 'brew',
94
168
  args: ['install', '--cask', 'visual-studio-code'],
169
+ updateCommand: brewAvailable ? 'brew upgrade --cask visual-studio-code' : undefined,
170
+ updatePackageManager: 'Homebrew',
171
+ updateNote: brewAvailable ? '将在新终端中通过 brew upgrade 升级 VS Code。' : '未检测到 brew。',
172
+ updateExecutable: brewAvailable ? 'brew' : undefined,
173
+ updateArgs: brewAvailable ? ['upgrade', '--cask', 'visual-studio-code'] : undefined,
95
174
  };
96
175
  } else {
97
176
  const snapAvailable = hasCommand('snap', platform);
@@ -105,6 +184,12 @@ export function getToolInstallers(platform = process.platform, hasCommand = comm
105
184
  : '当前 Linux 环境未检测到 snap,请按照发行版对应的官方说明安装。',
106
185
  executable: 'sudo',
107
186
  args: ['snap', 'install', 'code', '--classic'],
187
+ // snap 的升级动词是 refresh
188
+ updateCommand: snapAvailable ? 'sudo snap refresh code' : undefined,
189
+ updatePackageManager: 'snap',
190
+ updateNote: snapAvailable ? '将在新终端中通过 snap refresh 升级 VS Code。' : '未检测到 snap。',
191
+ updateExecutable: snapAvailable ? 'sudo' : undefined,
192
+ updateArgs: snapAvailable ? ['snap', 'refresh', 'code'] : undefined,
108
193
  };
109
194
  }
110
195
 
@@ -130,6 +215,26 @@ export function getToolInstallers(platform = process.platform, hasCommand = comm
130
215
  'Invoke-RestMethod https://code.kimi.com/kimi-code/install.ps1 | Invoke-Expression',
131
216
  ].join('; ')
132
217
  : undefined,
218
+ // 升级:kimi 官方脚本幂等,已安装时执行会自动升级到最新版,直接复用同一脚本
219
+ updateCommand: platform === 'win32'
220
+ ? 'irm https://code.kimi.com/kimi-code/install.ps1 | iex'
221
+ : 'curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash',
222
+ updatePackageManager: 'Kimi Code 官方安装脚本',
223
+ updateNote: '官方安装脚本幂等,已安装时执行会自动升级到最新版本。',
224
+ updateKind: 'script',
225
+ updateExecutionCommand: platform === 'win32'
226
+ ? [
227
+ "if (-not (Get-Command Get-FileHash -ErrorAction SilentlyContinue)) {",
228
+ "function global:Get-FileHash { param([string]$Path, [string]$Algorithm = 'SHA256')",
229
+ "$stream = [System.IO.File]::OpenRead($Path)",
230
+ "try { $sha = [System.Security.Cryptography.SHA256]::Create(); try { $bytes = $sha.ComputeHash($stream) } finally { $sha.Dispose() } } finally { $stream.Dispose() }",
231
+ "$hash = ([System.BitConverter]::ToString($bytes)).Replace('-', '')",
232
+ "New-Object PSObject -Property @{ Algorithm = 'SHA256'; Hash = $hash; Path = $Path }",
233
+ "}",
234
+ "}",
235
+ 'Invoke-RestMethod https://code.kimi.com/kimi-code/install.ps1 | Invoke-Expression',
236
+ ].join('; ')
237
+ : undefined,
133
238
  };
134
239
  installers.zcode = {
135
240
  supported: false,
@@ -137,6 +242,10 @@ export function getToolInstallers(platform = process.platform, hasCommand = comm
137
242
  packageManager: 'ZCode 官方安装包',
138
243
  docsUrl: TOOL_DOCS_URLS.zcode,
139
244
  note: 'ZCode 是桌面应用,官方提供 Windows、macOS 和 Linux 安装包,没有官方 npm CLI。',
245
+ // ZCode 是桌面应用、没有 CLI 更新通道;/api/update-tool 会对它返回 400
246
+ updateCommand: undefined,
247
+ updatePackageManager: 'ZCode 官方安装包',
248
+ updateNote: 'ZCode 是桌面应用,升级请到官网下载新版安装包覆盖安装。',
140
249
  };
141
250
  return installers;
142
251
  }
@@ -283,17 +392,34 @@ async function launchDsh(dirPath) {
283
392
  return launchInTerminal(dirPath, executable, ['web']);
284
393
  }
285
394
 
286
- async function findDshExecutable() {
395
+ // 在 Windows 上找 dsh 可执行文件。
396
+ // 优先用 where.exe dsh(覆盖 nvm4w 的 C:\nvm4w\nodejs\、默认的 %APPDATA%\npm、
397
+ // pnpm/yarn 自定义 prefix、以及用户手摆到 PATH 里的任意位置),
398
+ //再兜底 APPDATA\npm + npm prefix -g 两个写死路径(为 where.exe 漏检的边缘情况,
399
+ //比如 PATH 里只有 junction/symlink 时)。
400
+ export async function findDshExecutable() {
287
401
  if (process.platform !== 'win32') return commandExists('dsh') ? 'dsh' : null;
288
402
 
289
- const candidates = [path.join(process.env.APPDATA || '', 'npm', 'dsh.cmd')];
403
+ const where = spawnSync('where.exe', ['dsh'], { encoding: 'utf8', windowsHide: true });
404
+ if (where.status === 0 && where.stdout) {
405
+ for (const line of where.stdout.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)) {
406
+ // 偶尔会输出 "PATH=..." 这种变量行,跳过
407
+ if (/^[A-Z_]+=/i.test(line)) continue;
408
+ try {
409
+ const stat = await fs.stat(line);
410
+ if (stat.isFile()) return line;
411
+ } catch {}
412
+ }
413
+ }
414
+
415
+ const fallback = [path.join(process.env.APPDATA || '', 'npm', 'dsh.cmd')];
290
416
  const npmPrefix = spawnSync('npm.cmd', ['prefix', '-g'], {
291
417
  encoding: 'utf8',
292
418
  windowsHide: true,
293
419
  }).stdout?.trim();
294
- if (npmPrefix) candidates.push(path.join(npmPrefix, 'dsh.cmd'));
420
+ if (npmPrefix) fallback.push(path.join(npmPrefix, 'dsh.cmd'));
295
421
 
296
- for (const candidate of candidates.filter(Boolean)) {
422
+ for (const candidate of fallback.filter(Boolean)) {
297
423
  try {
298
424
  const stat = await fs.stat(candidate);
299
425
  if (stat.isFile()) return candidate;
@@ -390,27 +516,35 @@ async function findZCodeExecutable() {
390
516
  return null;
391
517
  }
392
518
 
393
- async function launchToolInstaller(installer, dirPath = process.cwd()) {
394
- if (installer.kind === 'script') {
519
+ async function launchToolInstaller(installer, dirPath = process.cwd(), { update = false } = {}) {
520
+ // update=true 时改用 updateXxx 字段(升级命令与安装命令动词不同:
521
+ // npm @latest / winget upgrade / brew upgrade / snap refresh / kimi 脚本幂等重跑)
522
+ const kindField = update ? 'updateKind' : 'kind';
523
+ const execCmdField = update ? 'updateExecutionCommand' : 'executionCommand';
524
+ const cmdField = update ? 'updateCommand' : 'command';
525
+ const executableField = update ? 'updateExecutable' : 'executable';
526
+ const argsField = update ? 'updateArgs' : 'args';
527
+
528
+ if (installer[kindField] === 'script') {
395
529
  if (process.platform === 'win32') {
396
530
  // 显式创建新控制台;编码脚本可避免 cmd.exe 截获 PowerShell 管道符。
397
- const encoded = Buffer.from(installer.executionCommand || installer.command, 'utf16le').toString('base64');
531
+ const encoded = Buffer.from(installer[execCmdField] || installer[cmdField], 'utf16le').toString('base64');
398
532
  return spawnDetached('cmd.exe', [
399
533
  '/c', 'start', '""', 'powershell.exe', '-NoLogo', '-NoExit',
400
534
  '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded,
401
535
  ], { cwd: dirPath });
402
536
  }
403
- return launchInTerminal(dirPath, 'bash', ['-lc', installer.command]);
537
+ return launchInTerminal(dirPath, 'bash', ['-lc', installer[cmdField]]);
404
538
  }
405
539
  if (process.platform === 'win32') {
406
- // 安装命令完全来自服务端白名单。使用可见 cmd 窗口,让用户看到进度和错误。
407
- const commandLine = [installer.executable, ...installer.args].join(' ');
540
+ // 安装/更新命令完全来自服务端白名单。使用可见 cmd 窗口,让用户看到进度和错误。
541
+ const commandLine = [installer[executableField], ...installer[argsField]].join(' ');
408
542
  return spawnDetached('cmd.exe', ['/c', 'start', '', 'cmd.exe', '/k', commandLine], {
409
543
  cwd: dirPath,
410
544
  });
411
545
  }
412
546
 
413
- return launchInTerminal(dirPath, installer.executable, installer.args);
547
+ return launchInTerminal(dirPath, installer[executableField], installer[argsField]);
414
548
  }
415
549
 
416
550
  export function registerFileOpenRoutes({
@@ -752,30 +886,113 @@ export function registerFileOpenRoutes({
752
886
  });
753
887
  }));
754
888
 
755
- // 检测本地工具是否已安装(供前端根据结果决定是否显示对应按钮)
756
- // 检测方式: spawn 'tool --version',exit 0 即视为已安装
889
+ // 更新本地工具:与 install-tool 同一套白名单,只是换用 updateXxx 命令变体
890
+ // (npm @latest / winget upgrade / brew upgrade / snap refresh / kimi 脚本重跑)。
891
+ // 没有 updateCommand 的工具(如 zcode 桌面应用)返回 400。
892
+ app.post('/api/update-tool', asyncRoute(async (req, res) => {
893
+ const { tool } = req.body || {};
894
+ const installers = getToolInstallers();
895
+ const installer = installers[tool];
896
+
897
+ if (!installer) {
898
+ throw new HttpError(400, '不支持的工具');
899
+ }
900
+ if (!installer.updateCommand) {
901
+ throw new HttpError(400, installer.updateNote || '当前工具不支持一键更新,请按官方文档手动升级');
902
+ }
903
+
904
+ await launchToolInstaller(installer, process.cwd(), { update: true });
905
+ res.json({
906
+ success: true,
907
+ message: '更新命令已在新终端中启动',
908
+ });
909
+ }));
910
+
911
+ // 查询 npm registry 上的最新版本,供前端更新菜单显示「当前 v1 → 最新 v2」。
912
+ // 只支持 TOOL_INSTALL_PACKAGES 里的 npm 工具;网络失败的工具返回 null(前端降级为原文案)。
913
+ // body 可选 { tools: ['claude', ...] } —— 缺省查全部 npm 工具。
914
+ app.post('/api/latest-tool-versions', asyncRoute(async (req, res) => {
915
+ const { tools } = req.body || {};
916
+ let requested;
917
+ if (tools === undefined) {
918
+ requested = Object.keys(TOOL_INSTALL_PACKAGES);
919
+ } else {
920
+ if (!Array.isArray(tools) || tools.some((t) => typeof t !== 'string')) {
921
+ throw new HttpError(400, 'tools 必须是字符串数组');
922
+ }
923
+ const unknown = tools.filter((t) => !TOOL_INSTALL_PACKAGES[t]);
924
+ if (unknown.length) {
925
+ throw new HttpError(400, `不支持的工具: ${unknown.join(', ')}`);
926
+ }
927
+ requested = tools;
928
+ }
929
+
930
+ const entries = await Promise.all(
931
+ requested.map(async (tool) => [tool, await fetchLatestVersion(TOOL_INSTALL_PACKAGES[tool])]),
932
+ );
933
+ res.json({
934
+ success: true,
935
+ latest: Object.fromEntries(entries),
936
+ });
937
+ }));
938
+
939
+ // 检测本地工具是否已安装 + 采集版本号(供前端决定按钮行为、tooltip 显示版本)
940
+ // 检测方式: spawn 'tool --version',exit 0 即视为已安装;stdout/stderr 里提取 semver。
757
941
  // 超时 15s:Claude CLI / opencode 等 Node 包装的工具首次冷启动
758
942
  // 在 Windows 上经常 5-7s(磁盘 cache + Defender 扫描),3s 永远命中超时。
759
943
  app.get('/api/check-tools', asyncRoute(async (req, res) => {
760
944
  const checkCmd = (cmd) => new Promise((resolve) => {
761
945
  const child = spawn(cmd, ['--version'], {
762
- stdio: 'ignore',
946
+ // 要捕获输出来解析版本号,不能再用 'ignore'
947
+ stdio: ['ignore', 'pipe', 'pipe'],
763
948
  shell: process.platform === 'win32',
764
949
  windowsHide: true,
765
950
  });
951
+ let output = '';
766
952
  let done = false;
767
953
  const finish = (ok) => {
768
954
  if (done) return;
769
955
  done = true;
770
956
  try { child.kill('SIGKILL'); } catch {}
771
- resolve(ok);
957
+ resolve({ installed: ok, version: parseVersionOutput(output) });
772
958
  };
959
+ // 有些 CLI 把版本号打到 stderr(如部分 Node 包装工具),两边都收
960
+ child.stdout.on('data', (chunk) => { output += chunk; });
961
+ child.stderr.on('data', (chunk) => { output += chunk; });
773
962
  child.on('error', () => finish(false));
774
963
  child.on('exit', (code) => finish(code === 0));
775
964
  setTimeout(() => finish(false), 15000);
776
965
  });
777
966
 
778
- const [vscode, claude, codex, opencode, kimiExecutable, zcodeExecutable, dshExecutable] = await Promise.all([
967
+ // dsh / kimi 走 find*Executable 拿到完整路径,再补一次 --version 采版本。
968
+ // zcode 是纯 GUI 桌面应用,ZCode.exe --version 行为未知(可能弹窗),不采集。
969
+ const versionOfExecutable = (executable) => {
970
+ if (!executable) return Promise.resolve(null);
971
+ return new Promise((resolve) => {
972
+ const child = spawn(executable, ['--version'], {
973
+ stdio: ['ignore', 'pipe', 'pipe'],
974
+ shell: process.platform === 'win32',
975
+ windowsHide: true,
976
+ });
977
+ let output = '';
978
+ let done = false;
979
+ const finish = () => {
980
+ if (done) return;
981
+ done = true;
982
+ try { child.kill('SIGKILL'); } catch {}
983
+ resolve(parseVersionOutput(output));
984
+ };
985
+ child.stdout.on('data', (chunk) => { output += chunk; });
986
+ child.stderr.on('data', (chunk) => { output += chunk; });
987
+ child.on('error', () => finish());
988
+ child.on('exit', () => finish());
989
+ setTimeout(() => finish(), 15000);
990
+ });
991
+ };
992
+
993
+ // dsh / kimi 的版本采集依赖 find 结果,用 then 链接保持全并行
994
+ // (find 会跑两次——一次拿路径一次链版本——都是纯 fs/注册表查询,开销可忽略)
995
+ const [vscodeRes, claudeRes, codexRes, opencodeRes, kimiExecutable, zcodeExecutable, dshExecutable, kimiVersion, dshVersion] = await Promise.all([
779
996
  checkCmd('code'),
780
997
  checkCmd('claude'),
781
998
  checkCmd('codex'),
@@ -783,6 +1000,8 @@ export function registerFileOpenRoutes({
783
1000
  findKimiExecutable(),
784
1001
  findZCodeExecutable(),
785
1002
  findDshExecutable(),
1003
+ findKimiExecutable().then(versionOfExecutable),
1004
+ findDshExecutable().then(versionOfExecutable),
786
1005
  ]);
787
1006
 
788
1007
  const installers = getToolInstallers();
@@ -790,13 +1009,22 @@ export function registerFileOpenRoutes({
790
1009
  success: true,
791
1010
  platform: process.platform,
792
1011
  installers: publicInstallerInfo(installers),
793
- vscode,
794
- claude,
795
- codex,
796
- opencode,
1012
+ vscode: vscodeRes.installed,
1013
+ claude: claudeRes.installed,
1014
+ codex: codexRes.installed,
1015
+ opencode: opencodeRes.installed,
797
1016
  kimi: !!kimiExecutable,
798
1017
  zcode: !!zcodeExecutable,
799
1018
  dsh: !!dshExecutable,
1019
+ versions: {
1020
+ vscode: vscodeRes.version,
1021
+ claude: claudeRes.version,
1022
+ codex: codexRes.version,
1023
+ opencode: opencodeRes.version,
1024
+ kimi: kimiExecutable ? kimiVersion : null,
1025
+ zcode: null, // 桌面应用,无 CLI 版本通道
1026
+ dsh: dshExecutable ? dshVersion : null,
1027
+ },
800
1028
  });
801
1029
  }));
802
1030
  }