zen-gitsync 2.17.8 → 2.17.10

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 (112) hide show
  1. package/README.md +6 -0
  2. package/package.json +2 -1
  3. package/src/cli/ai/agent.js +210 -58
  4. package/src/cli/ai/context.js +58 -0
  5. package/src/cli/ai/context.test.js +117 -0
  6. package/src/cli/ai/extensions.js +131 -0
  7. package/src/cli/ai/extensions.test.js +158 -0
  8. package/src/cli/ai/mcp.js +498 -0
  9. package/src/cli/ai/mcp.test.js +275 -0
  10. package/src/cli/ai/runtime.test.js +32 -0
  11. package/src/cli/ai/skills.js +207 -0
  12. package/src/cli/ai/skills.test.js +126 -0
  13. package/src/cli/ai/termui.js +1 -0
  14. package/src/cli/ai/tools.js +94 -3
  15. package/src/cli/ai/tools.test.js +82 -0
  16. package/src/cli/ai/transport.js +18 -4
  17. package/src/cli/ai/turn.js +21 -5
  18. package/src/config.js +27 -3
  19. package/src/paths.js +11 -0
  20. package/src/ui/public/assets/AgentView-BV15UtEF.css +1 -0
  21. package/src/ui/public/assets/AgentView-C-AJo7KS.js +5 -0
  22. package/src/ui/public/assets/AppVersionBadge-B6R6eN2N.js +2 -0
  23. package/src/ui/public/assets/BranchSelector-Cj3kMWON.js +1 -0
  24. package/src/ui/public/assets/CommitForm-DpXuFG-F.js +6 -0
  25. package/src/ui/public/assets/CommonDialog-BLPHDsn0.js +1 -0
  26. package/src/ui/public/assets/EditorView-B1OnoXXf.js +1 -0
  27. package/src/ui/public/assets/FileDiffViewer-BQl0JcWd.js +14 -0
  28. package/src/ui/public/assets/{FlowExecutionViewer-uXrzZjsW.js → FlowExecutionViewer-BgEt5_Hz.js} +1 -1
  29. package/src/ui/public/assets/FlowOrchestrationWorkspace-CbLmsSF8.js +21 -0
  30. package/src/ui/public/assets/IconButton-aM3vaSIN.js +1 -0
  31. package/src/ui/public/assets/LogList-UjoVFZ-H.js +7 -0
  32. package/src/ui/public/assets/MindmapView-D_04v0n4.js +1 -0
  33. package/src/ui/public/assets/MonitorView-BD1_uzq7.js +1 -0
  34. package/src/ui/public/assets/ProjectStartupButton-BTYlVvzh.js +1 -0
  35. package/src/ui/public/assets/RemoteManagerDialog-20mpQMwu.js +1 -0
  36. package/src/ui/public/assets/RemoteRepoCard-DOrm17Xd.js +1 -0
  37. package/src/ui/public/assets/SourceMapView-nUU9bYPh.js +3 -0
  38. package/src/ui/public/assets/SvgIcon-LUynX3Iq.js +1 -0
  39. package/src/ui/public/assets/TemplateManager-Bo3Q7J9Y.js +1 -0
  40. package/src/ui/public/assets/UserInputNode-D5tSXiJz.js +1 -0
  41. package/src/ui/public/assets/{WorkbenchView-v3QFn6Ln.css → WorkbenchView-C_Gmzvj1.css} +1 -1
  42. package/src/ui/public/assets/WorkbenchView-UGC334JL.js +13 -0
  43. package/src/ui/public/assets/_plugin-vue_export-helper-D-D9qkRz.js +19 -0
  44. package/src/ui/public/assets/configStore-BLRcYJdf.js +1 -0
  45. package/src/ui/public/assets/{element-plus-KTmRV-so.js → element-plus-DcZ-TVp6.js} +3 -3
  46. package/src/ui/public/assets/flow-mindmap-D1VTkZh3.js +139 -0
  47. package/src/ui/public/assets/{gitStore-CpcS-MzM.js → gitStore-DMEDMZMk.js} +4 -4
  48. package/src/ui/public/assets/index-CW3xs-af.css +1 -0
  49. package/src/ui/public/assets/index-S0_gRyiO.js +26 -0
  50. package/src/ui/public/assets/{office-docx-BfSBpdBv.js → office-docx-C0g5Oepv.js} +5 -5
  51. package/src/ui/public/assets/{office-excel-S95W2mv4.js → office-excel-D-uYLKaA.js} +1 -1
  52. package/src/ui/public/assets/{office-pptx-DFFarGrk.js → office-pptx-De3fnc6M.js} +1 -1
  53. package/src/ui/public/assets/previewSandbox-DXAxqDVu.js +0 -0
  54. package/src/ui/public/assets/toolsStore-CpImRUhI.js +1 -0
  55. package/src/ui/public/assets/{useThemeObserver-B01D-Vl6.js → useThemeObserver-CG8k8U18.js} +1 -1
  56. package/src/ui/public/assets/{vendor-dO4B7wQf.js → vendor-BIgZEtkx.js} +27 -27
  57. package/src/ui/public/assets/vue-flow-CPCFgenl.js +39 -0
  58. package/src/ui/public/index.html +17 -17
  59. package/src/ui/server/index.js +4 -0
  60. package/src/ui/server/routes/config.js +16 -3
  61. package/src/ui/server/routes/exec.js +19 -11
  62. package/src/ui/server/routes/fileOpen.js +142 -2
  63. package/src/ui/server/routes/remoteRepos.js +604 -0
  64. package/src/ui/server/routes/remoteRepos.test.js +386 -0
  65. package/src/ui/server/routes/workbench/agentChat.js +86 -180
  66. package/src/ui/server/routes/workbench/agentChatShared.test.js +151 -0
  67. package/src/ui/server/routes/workbench/agentMarketplace.js +986 -0
  68. package/src/ui/server/routes/workbench/agentMarketplace.test.js +306 -0
  69. package/src/ui/server/routes/workbench/agentParity.test.js +167 -0
  70. package/src/ui/server/routes/workbench/agentRoutes.js +86 -1
  71. package/src/ui/server/routes/workbench/agentRoutes.test.js +50 -1
  72. package/src/ui/server/routes/workbench/jobStore.js +17 -3
  73. package/src/ui/server/routes/workbench/orchestratorStore.js +42 -2
  74. package/src/ui/server/routes/workbench/projectRegistry.js +6 -0
  75. package/src/ui/server/routes/workbench/projectRegistry.test.js +70 -0
  76. package/src/ui/server/routes/workbench/projectTool.js +198 -0
  77. package/src/ui/server/routes/workbench/projectTool.test.js +152 -0
  78. package/src/ui/server/routes/workbench/remoteCliPromptParity.test.js +177 -0
  79. package/src/ui/server/routes/workbench/taskRunner.js +269 -12
  80. package/src/ui/server/routes/workbench/taskRunner.toolcalls.test.js +307 -0
  81. package/src/ui/server/socket/registerUiSocketHandlers.js +18 -11
  82. package/src/utils/shellPath.js +253 -0
  83. package/src/utils/shellPath.test.js +221 -0
  84. package/src/ui/public/assets/AgentView-CdJaeRh0.js +0 -5
  85. package/src/ui/public/assets/AgentView-DAY4S5WN.css +0 -1
  86. package/src/ui/public/assets/AppVersionBadge-CiijSYfM.js +0 -2
  87. package/src/ui/public/assets/BranchSelector-BS7ajwvv.js +0 -1
  88. package/src/ui/public/assets/CommitForm-CEOSqXCA.js +0 -6
  89. package/src/ui/public/assets/CommonDialog-CAaEi-4T.js +0 -1
  90. package/src/ui/public/assets/EditorView-DyaFyRDD.js +0 -1
  91. package/src/ui/public/assets/FileDiffViewer-BvX9cQjR.js +0 -14
  92. package/src/ui/public/assets/FlowOrchestrationWorkspace-DIBpUSt_.js +0 -21
  93. package/src/ui/public/assets/IconButton-CnoW2on6.js +0 -1
  94. package/src/ui/public/assets/LogList-D2espSVo.js +0 -7
  95. package/src/ui/public/assets/MindmapView-CZF-rZVs.js +0 -1
  96. package/src/ui/public/assets/MonitorView-DjLr7qOG.js +0 -1
  97. package/src/ui/public/assets/ProjectStartupButton-DzsU-QIO.js +0 -1
  98. package/src/ui/public/assets/RemoteManagerDialog-CuuG2ucT.js +0 -1
  99. package/src/ui/public/assets/RemoteRepoCard-BGf8L0of.js +0 -1
  100. package/src/ui/public/assets/SourceMapView-CFb02m6z.js +0 -3
  101. package/src/ui/public/assets/SvgIcon-SRAeYjnN.js +0 -1
  102. package/src/ui/public/assets/TemplateManager-gedqAXl8.js +0 -1
  103. package/src/ui/public/assets/UserInputNode-BOCuMQW6.js +0 -1
  104. package/src/ui/public/assets/WorkbenchView-CRoR21LQ.js +0 -13
  105. package/src/ui/public/assets/_plugin-vue_export-helper-C8OE7U9H.js +0 -19
  106. package/src/ui/public/assets/configStore-CuMElTgV.js +0 -1
  107. package/src/ui/public/assets/flow-mindmap-CurCIiCp.js +0 -139
  108. package/src/ui/public/assets/index-cApp6w0b.css +0 -1
  109. package/src/ui/public/assets/index-yXm9JLGv.js +0 -24
  110. package/src/ui/public/assets/previewSandbox-DkXYEpLP.js +0 -0
  111. package/src/ui/public/assets/toolsStore-DGxlRe3Y.js +0 -1
  112. package/src/ui/public/assets/vue-flow-CsvDD38k.js +0 -39
@@ -41,6 +41,7 @@ import { registerGitRoutes } from './routes/git.js';
41
41
  import { registerFsRoutes } from './routes/fs.js';
42
42
  import { registerNpmRoutes } from './routes/npm.js';
43
43
  import { registerFileOpenRoutes } from './routes/fileOpen.js';
44
+ import { registerRemoteReposRoutes } from './routes/remoteRepos.js';
44
45
  import { registerWorkbenchRoutes } from './routes/workbench.js';
45
46
  import { registerGitOpsRoutes } from './routes/gitOps.js';
46
47
  import { registerCodeRoutes } from './routes/code.js';
@@ -404,6 +405,9 @@ async function startUIServer(noOpen = false, savePort = false) {
404
405
  app
405
406
  });
406
407
 
408
+ // 远程托管平台仓库列表(GitHub / Gitee 两个 Tab 的数据源)
409
+ registerRemoteReposRoutes({ app });
410
+
407
411
  registerWorkbenchRoutes({
408
412
  app,
409
413
  getCurrentProjectPath: () => currentProjectPath,
@@ -986,10 +986,10 @@ export function registerConfigRoutes({
986
986
  }
987
987
  })
988
988
 
989
- // 保存通用设置(主题、语言、工作台任务执行器)
989
+ // 保存通用设置(主题、语言、工作台任务执行器、任务完成提示)
990
990
  app.post('/api/config/save-general-settings', express.json(), async (req, res) => {
991
991
  try {
992
- const { theme, locale, taskExecutor } = req.body
992
+ const { theme, locale, taskExecutor, notifyOnTaskDone } = req.body
993
993
 
994
994
  // 读取原始配置以保留项目设置
995
995
  const rawConfig = await configManager.readRawConfigFile()
@@ -1007,9 +1007,22 @@ export function registerConfigRoutes({
1007
1007
  rawConfig.taskExecutor = normalizedExecutor
1008
1008
  }
1009
1009
 
1010
+ // 任务执行结束提示开关(全局,默认关)。只接受布尔值,其它类型静默忽略,
1011
+ // 避免前端误传字符串 'false' 被当成真值落盘后永久打开。
1012
+ // ⚠️ 规范化函数对非法值返回的是 **null**(与 normalizeTaskExecutor 同语义),
1013
+ // 不是 undefined —— 判 `!== undefined` 会把非法值原样写进去变成 `null`。
1014
+ const normalizedNotify = configManager.normalizeNotifyOnTaskDone(notifyOnTaskDone)
1015
+ if (normalizedNotify !== null) {
1016
+ rawConfig.notifyOnTaskDone = normalizedNotify
1017
+ }
1018
+
1010
1019
  // 直接写入原始配置,避免覆盖项目设置
1011
1020
  await configManager.writeRawConfigFile(rawConfig)
1012
- res.json({ success: true, taskExecutor: normalizedExecutor || undefined })
1021
+ res.json({
1022
+ success: true,
1023
+ taskExecutor: normalizedExecutor || undefined,
1024
+ notifyOnTaskDone: normalizedNotify ?? undefined
1025
+ })
1013
1026
  } catch (error) {
1014
1027
  res.status(500).json({ success: false, error: error.message })
1015
1028
  }
@@ -18,6 +18,7 @@ import iconv from 'iconv-lite';
18
18
  import { spawn } from 'child_process';
19
19
  import { ensureWithinCwd } from '../utils/pathGuard.js';
20
20
  import { asyncRoute, HttpError } from '../utils/asyncRoute.js';
21
+ import { augmentEnvPath } from '../../../utils/shellPath.js';
21
22
 
22
23
  // ─────────────────────────────────────────────────────────────────────────────
23
24
  // 命令 → argv 拆分(SEC-INJ-2 修复)
@@ -92,16 +93,23 @@ function resolveBinAndArgs(command) {
92
93
  }
93
94
 
94
95
  // 通用 env 模板 — 颜色 + 终端兼容性,所有 exec 流式 / 交互式复用
95
- const CHILD_ENV_TEMPLATE = {
96
- ...process.env,
97
- GIT_CONFIG_PARAMETERS: "'color.ui=always' 'color.status=always' 'core.quotepath=false'",
98
- FORCE_COLOR: '3',
99
- NPM_CONFIG_COLOR: 'always',
100
- TERM: 'xterm-256color',
101
- COLORTERM: 'truecolor',
102
- CLICOLOR_FORCE: '1',
103
- PYTHONUNBUFFERED: '1',
104
- };
96
+ //
97
+ // PATH 交给 augmentEnvPath 补一次注册表最新值:服务端进程的环境块是**启动快照**,
98
+ // 用户在这个进程活着期间装的 CLI 不在里面,表现是"GUI 里能探测到、执行却报
99
+ // '不是内部或外部命令'"(2026-09-22 实测 gh)。详见 utils/shellPath.js。
100
+ // 因为要读注册表,这里必须是函数而不能是模块级常量 —— 常量会在 import 时定死。
101
+ async function buildChildEnv() {
102
+ return augmentEnvPath({
103
+ ...process.env,
104
+ GIT_CONFIG_PARAMETERS: "'color.ui=always' 'color.status=always' 'core.quotepath=false'",
105
+ FORCE_COLOR: '3',
106
+ NPM_CONFIG_COLOR: 'always',
107
+ TERM: 'xterm-256color',
108
+ COLORTERM: 'truecolor',
109
+ CLICOLOR_FORCE: '1',
110
+ PYTHONUNBUFFERED: '1',
111
+ });
112
+ }
105
113
 
106
114
  function isWindowsBuiltinCommand(command) {
107
115
  if (process.platform !== 'win32') return false;
@@ -157,7 +165,7 @@ export function registerExecRoutes({
157
165
  // spawn(bin, argv, { shell: false }) — 不走 shell,无注入
158
166
  const childProcess = spawn(bin, args, {
159
167
  cwd: execDirectory,
160
- env: CHILD_ENV_TEMPLATE,
168
+ env: await buildChildEnv(),
161
169
  });
162
170
 
163
171
  runningProcesses.set(processId, {
@@ -20,6 +20,7 @@ import open from 'open';
20
20
  import { spawn, spawnSync, execFile } from 'child_process';
21
21
  import { fileURLToPath } from 'node:url';
22
22
  import { promisify } from 'node:util';
23
+ import { augmentEnvPath, pathValueOf, withPathValue } from '../../../utils/shellPath.js';
23
24
 
24
25
  const execFileAsync = promisify(execFile);
25
26
 
@@ -38,6 +39,10 @@ const TOOL_DOCS_URLS = Object.freeze({
38
39
  kimi: 'https://moonshotai.github.io/kimi-code/en/guides/getting-started',
39
40
  zcode: 'https://zcode.z.ai/',
40
41
  dsh: 'https://github.com/deepseek-ai/deepseek-harness',
42
+ // 远程仓库列表页(RemoteReposList)用到的两个托管平台 CLI。它们不是"AI 工具",
43
+ // 不参与 /api/check-tools 的 open-with 菜单,只借这里的白名单走 /api/install-tool。
44
+ gh: 'https://cli.github.com/',
45
+ gitee: 'https://gitee.com/oschina/gitee-cli',
41
46
  });
42
47
 
43
48
  function commandExists(command, platform = process.platform) {
@@ -251,10 +256,91 @@ export function getToolInstallers(platform = process.platform, hasCommand = comm
251
256
  updatePackageManager: 'ZCode 官方安装包',
252
257
  updateNote: 'ZCode 是桌面应用,升级请到官网下载新版安装包覆盖安装。',
253
258
  };
259
+
260
+ // ── 远程托管平台 CLI(远程仓库列表页用)──────────────────────────────────
261
+ // 与上面的 AI 工具共用同一份白名单,好处是"前端只能提交 tool id、命令由服务端
262
+ // 决定"这条安全边界不用再实现一遍。它们不出现在 open-with 菜单里 —— 那个菜单
263
+ // 是 useDirectoryOpenActions.ts 里的硬编码数组,不看这张表。
264
+ if (platform === 'win32') {
265
+ const wingetAvailable = hasCommand('winget', platform);
266
+ installers.gh = {
267
+ supported: wingetAvailable,
268
+ command: 'winget install --id GitHub.cli -e --accept-package-agreements --accept-source-agreements',
269
+ packageManager: 'winget',
270
+ docsUrl: TOOL_DOCS_URLS.gh,
271
+ note: wingetAvailable
272
+ ? '将在新终端中通过 winget 安装 GitHub CLI;装完请在新终端执行 gh auth login 登录。'
273
+ : '未检测到 winget,请到 cli.github.com 下载安装包。',
274
+ executable: 'winget',
275
+ args: ['install', '--id', 'GitHub.cli', '-e', '--accept-package-agreements', '--accept-source-agreements'],
276
+ updateCommand: wingetAvailable
277
+ ? 'winget upgrade --id GitHub.cli -e --accept-package-agreements --accept-source-agreements'
278
+ : undefined,
279
+ updatePackageManager: 'winget',
280
+ updateNote: wingetAvailable ? '将在新终端中通过 winget upgrade 升级 GitHub CLI。' : '未检测到 winget。',
281
+ updateExecutable: wingetAvailable ? 'winget' : undefined,
282
+ updateArgs: wingetAvailable
283
+ ? ['upgrade', '--id', 'GitHub.cli', '-e', '--accept-package-agreements', '--accept-source-agreements']
284
+ : undefined,
285
+ };
286
+ } else if (platform === 'darwin') {
287
+ const brewAvailable = hasCommand('brew', platform);
288
+ installers.gh = {
289
+ supported: brewAvailable,
290
+ command: 'brew install gh',
291
+ packageManager: 'Homebrew',
292
+ docsUrl: TOOL_DOCS_URLS.gh,
293
+ note: brewAvailable
294
+ ? '将在新终端中通过 Homebrew 安装 GitHub CLI;装完请执行 gh auth login 登录。'
295
+ : '未检测到 Homebrew,请到 cli.github.com 下载安装包。',
296
+ executable: 'brew',
297
+ args: ['install', 'gh'],
298
+ updateCommand: brewAvailable ? 'brew upgrade gh' : undefined,
299
+ updatePackageManager: 'Homebrew',
300
+ updateNote: brewAvailable ? '将在新终端中通过 brew upgrade 升级 GitHub CLI。' : '未检测到 brew。',
301
+ updateExecutable: brewAvailable ? 'brew' : undefined,
302
+ updateArgs: brewAvailable ? ['upgrade', 'gh'] : undefined,
303
+ };
304
+ } else {
305
+ // Debian/Ubuntu 官方源里已有 gh;其它发行版请看 docsUrl(Arch: pacman -S github-cli)
306
+ const aptAvailable = hasCommand('apt-get', platform);
307
+ installers.gh = {
308
+ supported: aptAvailable,
309
+ command: 'sudo apt-get install -y gh',
310
+ packageManager: 'apt',
311
+ docsUrl: TOOL_DOCS_URLS.gh,
312
+ note: aptAvailable
313
+ ? '将在新终端中通过 apt 安装 GitHub CLI,可能需要输入系统密码;装完请执行 gh auth login 登录。'
314
+ : '当前 Linux 环境未检测到 apt-get,请按 docsUrl 里对应发行版的说明安装。',
315
+ executable: 'sudo',
316
+ args: ['apt-get', 'install', '-y', 'gh'],
317
+ // apt 没有"就地升级单个包"的稳定动词,不给一键更新,引导去官方文档
318
+ updateCommand: undefined,
319
+ updatePackageManager: 'apt',
320
+ updateNote: '请按官方文档升级 GitHub CLI。',
321
+ };
322
+ }
323
+ installers.gitee = {
324
+ supported: npmAvailable,
325
+ command: 'npm install -g @gitee/gitee-cli',
326
+ packageManager: 'npm',
327
+ docsUrl: TOOL_DOCS_URLS.gitee,
328
+ note: npmAvailable
329
+ ? '将在新终端中通过 npm 全局安装 Gitee CLI;装完请执行 gitee auth login 粘贴私人令牌。'
330
+ : '未检测到 npm,请先安装 Node.js/npm,或按官方文档手动安装。',
331
+ executable: npmExecutable,
332
+ args: ['install', '-g', '@gitee/gitee-cli'],
333
+ updateCommand: 'npm install -g @gitee/gitee-cli@latest',
334
+ updatePackageManager: 'npm',
335
+ updateNote: '将在新终端中执行 `npm install -g @gitee/gitee-cli@latest`。已是最新时 npm 会跳过。',
336
+ updateExecutable: npmExecutable,
337
+ updateArgs: ['install', '-g', '@gitee/gitee-cli@latest'],
338
+ };
339
+
254
340
  return installers;
255
341
  }
256
342
 
257
- function publicInstallerInfo(installers) {
343
+ export function publicInstallerInfo(installers) {
258
344
  return Object.fromEntries(Object.entries(installers).map(([tool, installer]) => [tool, {
259
345
  supported: installer.supported,
260
346
  command: installer.command,
@@ -393,7 +479,13 @@ export async function launchGai(dirPath, {
393
479
  '-WorkingDirectory (Get-Location).ProviderPath -WindowStyle Normal',
394
480
  ].join(' ');
395
481
  return spawnFn('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand', encode(launcher)], {
396
- cwd: dirPath, windowsHide: true, timeout: 10000,
482
+ cwd: dirPath,
483
+ windowsHide: true,
484
+ timeout: 10000,
485
+ // 这个终端窗口以及里面跑起来的 g ai,会继承下面这份 env;把注册表里新装的 CLI
486
+ // 目录补进去,否则窗口里 `gh repo list` 照样报"不是内部或外部命令"
487
+ // (本进程的 PATH 是启动快照)。见 utils/shellPath.js 顶部。
488
+ env: await augmentEnvPath(process.env),
397
489
  });
398
490
  }
399
491
 
@@ -577,6 +669,54 @@ async function launchToolInstaller(installer, dirPath = process.cwd(), { update
577
669
  return launchInTerminal(dirPath, installer[executableField], installer[argsField]);
578
670
  }
579
671
 
672
+ /**
673
+ * 在新终端窗口里跑一条**服务端白名单内的**命令,跑完窗口不自动关。
674
+ *
675
+ * 与 launchToolInstaller 的区别:那个吃 installer 描述符(executable + args),
676
+ * 这个直接吃整条命令串。给「登录」这类**交互式** CLI 用 —— `gh auth login`
677
+ * 会走一串问答(选 GitHub.com / 协议 / 浏览器还是粘贴 token),必须在真终端里跑:
678
+ * 被 spawn 出去又没有 TTY 的话,它直接就失败了,用户屏幕上什么也看不到。
679
+ *
680
+ * Windows 用 `cmd /k`:登录完成后窗口留着,用户能看清结果;失败也能看到报错。
681
+ *
682
+ * ⚠️ command 必须是服务端自己拼出来的常量。永远不要把它接成请求体里的字符串 ——
683
+ * 那等于开了一个任意命令执行的口子(和 /api/install-tool 同一条原则)。
684
+ *
685
+ * `pathDirs` 是**必须**看的参数(2026-09-22 实测踩到):
686
+ * 本进程的 `process.env.PATH` 是"启动那一刻"的快照。用户在界面里刚装好 gh,
687
+ * 安装器改的是**注册表**里的 PATH,我们这个已经跑着的 node 进程看不见 ——
688
+ * 于是出现很割裂的一幕:**探测**能找到 gh(它走注册表 + 默认落点),界面显示
689
+ * "gh 2.101.0 已安装",可 `cmd /k gh auth login` 这个子进程按自己的 PATH 找,
690
+ * 回一句 `'gh' 不是内部或外部命令`;而用户自己新开一个 cmd 却是好的,
691
+ * 因为新进程拿到的是最新 PATH。
692
+ * 解法有两层,缺一不可:
693
+ * 1. **基线兜底**(`augmentEnvPath`):把注册表 Machine + User 的 PATH 里本进程
694
+ * 快照没有的目录补进子进程 —— 语义上等价于"这个进程是现在才启动的"。
695
+ * 所以**不传 `pathDirs` 也不会踩旧快照**,`launchGai` 这类入口不用各自记得传。
696
+ * 2. `pathDirs`:调用方**已经知道 CLI 落点**时显式**前置**,抢在别的同名命令之前。
697
+ * 注意两层语义不同:兜底是追加(不动原有优先级),pathDirs 是前置(要优先)。
698
+ * 比起"把可执行文件全路径拼进命令行",前置 PATH 还顺带绕开了 cmd.exe 那套
699
+ * "路径带空格时的嵌套引号"地狱(`C:\Program Files\GitHub CLI\gh.exe`)。
700
+ */
701
+ export async function launchCommandInTerminal(command, { cwd = process.cwd(), pathDirs = [] } = {}) {
702
+ const dirs = pathDirs.filter(Boolean);
703
+ const augmented = await augmentEnvPath(process.env);
704
+ let env;
705
+ if (dirs.length) {
706
+ env = withPathValue(augmented, [...dirs, pathValueOf(augmented)].filter(Boolean).join(path.delimiter));
707
+ } else {
708
+ // 没有要补的就传 undefined,让子进程原样继承父环境(与改动前行为一致)
709
+ env = augmented === process.env ? undefined : augmented;
710
+ }
711
+
712
+ if (process.platform === 'win32') {
713
+ return spawnDetached('cmd.exe', ['/c', 'start', '', 'cmd.exe', '/k', command], { cwd, env });
714
+ }
715
+ // posix 侧不额外处理 PATH:那边没有"安装器改注册表"这套机制,
716
+ // 终端模拟器直接继承图形会话的环境,不会出现上面的错配。
717
+ return launchInTerminal(cwd, 'bash', ['-lc', command]);
718
+ }
719
+
580
720
  export function registerFileOpenRoutes({
581
721
  app,
582
722
  launchAi = launchGai,