zen-gitsync 2.16.24 → 2.16.27

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 (86) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/cli/ai/platformGuard.js +200 -200
  4. package/src/cli/ai/platformGuard.test.js +253 -253
  5. package/src/gitCommit.js +484 -484
  6. package/src/ui/public/assets/AgentView-Bytr0HbV.js +4 -0
  7. package/src/ui/public/assets/AppVersionBadge-BW4vXJxH.js +2 -0
  8. package/src/ui/public/assets/BranchSelector-DsLQi1AR.js +1 -0
  9. package/src/ui/public/assets/CommitForm-CmmxW73q.js +6 -0
  10. package/src/ui/public/assets/CommonDialog-CQz6iEqp.js +1 -0
  11. package/src/ui/public/assets/EditorView-CtygNTuz.js +1 -0
  12. package/src/ui/public/assets/{FlowExecutionViewer-BLv0xSX3.js → FlowExecutionViewer-B7CdeiSp.js} +1 -1
  13. package/src/ui/public/assets/FlowOrchestrationWorkspace-DtTItIzf.js +21 -0
  14. package/src/ui/public/assets/LogList-Bo53PBwg.css +1 -0
  15. package/src/ui/public/assets/LogList-Cwem9ONv.js +7 -0
  16. package/src/ui/public/assets/MindmapView-DR5bT_oQ.js +2 -0
  17. package/src/ui/public/assets/MonitorView-DoLNabxl.js +1 -0
  18. package/src/ui/public/assets/ProjectStartupButton-BiNiH43W.js +1 -0
  19. package/src/ui/public/assets/{RemoteRepoCard-LHrI3uIa.js → RemoteRepoCard-DJ7ficOj.js} +2 -2
  20. package/src/ui/public/assets/SourceMapView-CSVCaU8d.js +3 -0
  21. package/src/ui/public/assets/{SvgIcon-BfPnov-Z.js → SvgIcon-CL2qPswh.js} +1 -1
  22. package/src/ui/public/assets/UserInputNode-BftMuqji.js +1 -0
  23. package/src/ui/public/assets/WorkbenchView-Bp2YaSGS.css +1 -0
  24. package/src/ui/public/assets/WorkbenchView-Cl9H5uXg.js +28 -0
  25. package/src/ui/public/assets/{_plugin-vue_export-helper-BpqSqjxA.js → _plugin-vue_export-helper-D3_upOKo.js} +3 -3
  26. package/src/ui/public/assets/configStore-Cqnbpz5-.js +1 -0
  27. package/src/ui/public/assets/{css.worker-Wv5dxAWO.js → css.worker-CvXBzhp8.js} +1 -1
  28. package/src/ui/public/assets/{dagre-DgwsqYn1.js → dagre-CWIGcX9d.js} +1 -1
  29. package/src/ui/public/assets/element-plus-B3pTS_5R.js +32 -0
  30. package/src/ui/public/assets/element-plus-CQrjg9us.css +1 -0
  31. package/src/ui/public/assets/{flow-mindmap-ByUzqcTH.js → flow-mindmap-B5aqOviR.js} +38 -38
  32. package/src/ui/public/assets/{html.worker-CQP8QQsS.js → html.worker-BO6WuOEO.js} +1 -1
  33. package/src/ui/public/assets/index-D89M9m30.js +71 -0
  34. package/src/ui/public/assets/{json.worker-DzV-CpCQ.js → json.worker-BkJRGcCJ.js} +1 -1
  35. package/src/ui/public/assets/{monaco-Dje8Rg13.js → monaco-BFLoT2VD.js} +198 -198
  36. package/src/ui/public/assets/socket-io-Bcae2P7B.js +1 -0
  37. package/src/ui/public/assets/toolsStore-RkALTxME.js +1 -0
  38. package/src/ui/public/assets/{ts.worker-Dth06zuC.js → ts.worker-B0J26iPs.js} +15 -15
  39. package/src/ui/public/assets/{vendor-DKiix7Qv.js → vendor-Cx8PA1NP.js} +14 -14
  40. package/src/ui/public/assets/{vue-flow-B-YyhjTY.js → vue-flow-COROsN-V.js} +2 -2
  41. package/src/ui/public/favicon.svg +75 -75
  42. package/src/ui/public/index.html +31 -30
  43. package/src/ui/public/logo.svg +74 -74
  44. package/src/ui/server/.claude/codediff.txt +8 -0
  45. package/src/ui/server/index.js +2 -1
  46. package/src/ui/server/middleware/requestLogger.js +53 -53
  47. package/src/ui/server/routes/branchStatus.js +105 -105
  48. package/src/ui/server/routes/fileOpen.js +171 -1
  49. package/src/ui/server/routes/fs.js +2 -2
  50. package/src/ui/server/routes/git/diff.js +426 -426
  51. package/src/ui/server/routes/git/diffUtils.js +149 -149
  52. package/src/ui/server/routes/git/stash.js +550 -550
  53. package/src/ui/server/routes/git/tags.js +176 -176
  54. package/src/ui/server/routes/gitOps.js +10 -0
  55. package/src/ui/server/routes/process.js +86 -86
  56. package/src/ui/server/routes/status.js +67 -67
  57. package/src/ui/server/routes/workbench/pdfText.js +145 -145
  58. package/src/ui/server/utils/createSavePortToFile.js +48 -48
  59. package/src/ui/server/utils/instanceRegistry.js +282 -282
  60. package/src/ui/server/utils/pathGuard.js +21 -0
  61. package/src/ui/server/utils/startServerOnAvailablePort.js +116 -116
  62. package/src/utils/index.js +6 -3
  63. package/src/utils/utils.test.js +89 -2
  64. package/src/ui/public/assets/AgentView-B1P7BpdL.js +0 -4
  65. package/src/ui/public/assets/AppVersionBadge-CxbNViwm.js +0 -2
  66. package/src/ui/public/assets/BranchSelector-D4dKHxxK.js +0 -1
  67. package/src/ui/public/assets/CommitForm-klcrUCW7.js +0 -6
  68. package/src/ui/public/assets/CommonDialog-DfPvpafn.js +0 -1
  69. package/src/ui/public/assets/EditorView-CZofJ-wy.js +0 -1
  70. package/src/ui/public/assets/FlowOrchestrationWorkspace-BcPCI3Cq.js +0 -21
  71. package/src/ui/public/assets/LogList-BYrHPSZw.js +0 -15
  72. package/src/ui/public/assets/LogList-CeFFytjB.css +0 -1
  73. package/src/ui/public/assets/MindmapView-CNd4Q8B8.js +0 -2
  74. package/src/ui/public/assets/MonitorView-BlmWenOX.js +0 -1
  75. package/src/ui/public/assets/ProjectStartupButton-706xOj0N.js +0 -1
  76. package/src/ui/public/assets/SourceMapView-e96cgLn7.js +0 -3
  77. package/src/ui/public/assets/UserInputNode-W1uGFT5P.js +0 -1
  78. package/src/ui/public/assets/WorkbenchView-Cfp_bLC8.js +0 -28
  79. package/src/ui/public/assets/WorkbenchView-DFO8IfcU.css +0 -1
  80. package/src/ui/public/assets/configStore-CqqnJlVS.js +0 -1
  81. package/src/ui/public/assets/element-plus--ayTjGvF.js +0 -25
  82. package/src/ui/public/assets/element-plus-D3Dn7nX0.css +0 -1
  83. package/src/ui/public/assets/index-BNymf6-p.js +0 -71
  84. package/src/ui/public/assets/socket-io-CGezBd--.js +0 -1
  85. /package/src/ui/public/assets/{editor.worker-Bd9IXS8d.js → editor.worker-Cn2oRESe.js} +0 -0
  86. /package/src/ui/public/assets/{rolldown-runtime-BM3Ffeng.js → rolldown-runtime-CMxvf4Kt.js} +0 -0
@@ -1,105 +1,105 @@
1
- // Copyright 2026 xz333221
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
- //
15
- import { asyncRoute, HttpError } from '../utils/asyncRoute.js'
16
-
17
- import logger from '../utils/logger.js'
18
-
19
- export function registerBranchStatusRoutes({
20
- app,
21
- execGitCommand,
22
- getIsGitRepo,
23
- getBranchStatusCache,
24
- setBranchStatusCache,
25
- getRecentPushStatus,
26
- setRecentPushStatus
27
- }) {
28
- // 获取当前分支 - 直接读取,不缓存(git symbolic-ref 极快,<5ms)
29
- app.get('/api/branch', asyncRoute(async (req, res) => {
30
- try {
31
- const { stdout } = await execGitCommand(['symbolic-ref', '--short', 'HEAD']);
32
- res.json({ branch: stdout.trim() });
33
- } catch (error) {
34
- res.status(500).json({ error: error.message });
35
- }
36
- }));
37
-
38
- // 获取分支与远程的差异状态(领先/落后提交数)
39
- app.get('/api/branch-status', asyncRoute(async (req, res) => {
40
- try {
41
- if (!getIsGitRepo()) {
42
- return res.json({ hasUpstream: false, ahead: 0, behind: 0 });
43
- }
44
-
45
- const now = Date.now();
46
- const forceRefresh = req.query.force === 'true';
47
-
48
- const recentPushStatus = getRecentPushStatus();
49
- const branchStatusCache = getBranchStatusCache();
50
-
51
- // push 后10秒内直接返回同步状态
52
- if (recentPushStatus.justPushed &&
53
- (now - recentPushStatus.pushTime) < recentPushStatus.validDuration) {
54
- logger.info('检测到最近推送过,直接返回同步状态');
55
- return res.json({
56
- hasUpstream: true,
57
- upstreamBranch: branchStatusCache.upstreamBranch || 'origin/main',
58
- ahead: 0,
59
- behind: 0
60
- });
61
- }
62
-
63
- // 5秒缓存分支名,防止短时间内重复执行 git symbolic-ref / git rev-parse
64
- const branchInfoCacheValid = !forceRefresh &&
65
- branchStatusCache.currentBranch &&
66
- branchStatusCache.upstreamBranch &&
67
- (now - branchStatusCache.lastUpdate) < branchStatusCache.cacheTimeout;
68
-
69
- let currentBranch, upstreamBranch;
70
-
71
- if (branchInfoCacheValid) {
72
- currentBranch = branchStatusCache.currentBranch;
73
- upstreamBranch = branchStatusCache.upstreamBranch;
74
- logger.info(`使用5秒缓存的分支名: ${currentBranch} -> ${upstreamBranch}`);
75
- } else {
76
- // 直接读取,不再走5分钟长缓存
77
- const { stdout: branchOut } = await execGitCommand(['symbolic-ref', '--short', 'HEAD']);
78
- currentBranch = branchOut.trim();
79
-
80
- const { stdout: upstreamOut } = await execGitCommand(
81
- ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'],
82
- { ignoreError: true }
83
- );
84
- upstreamBranch = upstreamOut.trim() || null;
85
-
86
- if (!upstreamBranch) {
87
- setBranchStatusCache({ currentBranch: null, upstreamBranch: null, lastUpdate: 0, cacheTimeout: 5000 });
88
- return res.json({ hasUpstream: false, ahead: 0, behind: 0 });
89
- }
90
-
91
- setBranchStatusCache({ currentBranch, upstreamBranch, lastUpdate: now, cacheTimeout: 5000 });
92
- }
93
-
94
- const { stdout: aheadBehindOutput } = await execGitCommand(
95
- ['rev-list', '--left-right', '--count', `${currentBranch}...${upstreamBranch}`]
96
- );
97
- const [ahead, behind] = aheadBehindOutput.trim().split('\t').map(Number);
98
-
99
- res.json({ hasUpstream: true, upstreamBranch, ahead, behind });
100
- } catch (error) {
101
- logger.error('获取分支状态失败:', error);
102
- res.status(500).json({ error: error.message });
103
- }
104
- }));
105
- }
1
+ // Copyright 2026 xz333221
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+ import { asyncRoute, HttpError } from '../utils/asyncRoute.js'
16
+
17
+ import logger from '../utils/logger.js'
18
+
19
+ export function registerBranchStatusRoutes({
20
+ app,
21
+ execGitCommand,
22
+ getIsGitRepo,
23
+ getBranchStatusCache,
24
+ setBranchStatusCache,
25
+ getRecentPushStatus,
26
+ setRecentPushStatus
27
+ }) {
28
+ // 获取当前分支 - 直接读取,不缓存(git symbolic-ref 极快,<5ms)
29
+ app.get('/api/branch', asyncRoute(async (req, res) => {
30
+ try {
31
+ const { stdout } = await execGitCommand(['symbolic-ref', '--short', 'HEAD']);
32
+ res.json({ branch: stdout.trim() });
33
+ } catch (error) {
34
+ res.status(500).json({ error: error.message });
35
+ }
36
+ }));
37
+
38
+ // 获取分支与远程的差异状态(领先/落后提交数)
39
+ app.get('/api/branch-status', asyncRoute(async (req, res) => {
40
+ try {
41
+ if (!getIsGitRepo()) {
42
+ return res.json({ hasUpstream: false, ahead: 0, behind: 0 });
43
+ }
44
+
45
+ const now = Date.now();
46
+ const forceRefresh = req.query.force === 'true';
47
+
48
+ const recentPushStatus = getRecentPushStatus();
49
+ const branchStatusCache = getBranchStatusCache();
50
+
51
+ // push 后10秒内直接返回同步状态
52
+ if (recentPushStatus.justPushed &&
53
+ (now - recentPushStatus.pushTime) < recentPushStatus.validDuration) {
54
+ logger.info('检测到最近推送过,直接返回同步状态');
55
+ return res.json({
56
+ hasUpstream: true,
57
+ upstreamBranch: branchStatusCache.upstreamBranch || 'origin/main',
58
+ ahead: 0,
59
+ behind: 0
60
+ });
61
+ }
62
+
63
+ // 5秒缓存分支名,防止短时间内重复执行 git symbolic-ref / git rev-parse
64
+ const branchInfoCacheValid = !forceRefresh &&
65
+ branchStatusCache.currentBranch &&
66
+ branchStatusCache.upstreamBranch &&
67
+ (now - branchStatusCache.lastUpdate) < branchStatusCache.cacheTimeout;
68
+
69
+ let currentBranch, upstreamBranch;
70
+
71
+ if (branchInfoCacheValid) {
72
+ currentBranch = branchStatusCache.currentBranch;
73
+ upstreamBranch = branchStatusCache.upstreamBranch;
74
+ logger.info(`使用5秒缓存的分支名: ${currentBranch} -> ${upstreamBranch}`);
75
+ } else {
76
+ // 直接读取,不再走5分钟长缓存
77
+ const { stdout: branchOut } = await execGitCommand(['symbolic-ref', '--short', 'HEAD']);
78
+ currentBranch = branchOut.trim();
79
+
80
+ const { stdout: upstreamOut } = await execGitCommand(
81
+ ['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'],
82
+ { ignoreError: true }
83
+ );
84
+ upstreamBranch = upstreamOut.trim() || null;
85
+
86
+ if (!upstreamBranch) {
87
+ setBranchStatusCache({ currentBranch: null, upstreamBranch: null, lastUpdate: 0, cacheTimeout: 5000 });
88
+ return res.json({ hasUpstream: false, ahead: 0, behind: 0 });
89
+ }
90
+
91
+ setBranchStatusCache({ currentBranch, upstreamBranch, lastUpdate: now, cacheTimeout: 5000 });
92
+ }
93
+
94
+ const { stdout: aheadBehindOutput } = await execGitCommand(
95
+ ['rev-list', '--left-right', '--count', `${currentBranch}...${upstreamBranch}`]
96
+ );
97
+ const [ahead, behind] = aheadBehindOutput.trim().split('\t').map(Number);
98
+
99
+ res.json({ hasUpstream: true, upstreamBranch, ahead, behind });
100
+ } catch (error) {
101
+ logger.error('获取分支状态失败:', error);
102
+ res.status(500).json({ error: error.message });
103
+ }
104
+ }));
105
+ }
@@ -29,6 +29,8 @@ const TOOL_DOCS_URLS = Object.freeze({
29
29
  claude: 'https://docs.anthropic.com/en/docs/claude-code/getting-started',
30
30
  codex: 'https://help.openai.com/en/articles/11096431-openai-codex-cli-getting-started',
31
31
  opencode: 'https://opencode.ai/docs',
32
+ kimi: 'https://moonshotai.github.io/kimi-code/en/guides/getting-started',
33
+ zcode: 'https://zcode.z.ai/',
32
34
  });
33
35
 
34
36
  function commandExists(command, platform = process.platform) {
@@ -104,6 +106,36 @@ export function getToolInstallers(platform = process.platform, hasCommand = comm
104
106
  };
105
107
  }
106
108
 
109
+ installers.kimi = {
110
+ supported: platform === 'win32' || platform === 'darwin' || platform === 'linux',
111
+ command: platform === 'win32'
112
+ ? 'irm https://code.kimi.com/kimi-code/install.ps1 | iex'
113
+ : 'curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash',
114
+ packageManager: 'Kimi Code 官方安装脚本',
115
+ docsUrl: TOOL_DOCS_URLS.kimi,
116
+ note: '将打开终端执行官方安装脚本;Windows 首次使用还需要 Git for Windows。',
117
+ kind: 'script',
118
+ executionCommand: platform === 'win32'
119
+ ? [
120
+ "if (-not (Get-Command Get-FileHash -ErrorAction SilentlyContinue)) {",
121
+ "function global:Get-FileHash { param([string]$Path, [string]$Algorithm = 'SHA256')",
122
+ "$stream = [System.IO.File]::OpenRead($Path)",
123
+ "try { $sha = [System.Security.Cryptography.SHA256]::Create(); try { $bytes = $sha.ComputeHash($stream) } finally { $sha.Dispose() } } finally { $stream.Dispose() }",
124
+ "$hash = ([System.BitConverter]::ToString($bytes)).Replace('-', '')",
125
+ "New-Object PSObject -Property @{ Algorithm = 'SHA256'; Hash = $hash; Path = $Path }",
126
+ "}",
127
+ "}",
128
+ 'Invoke-RestMethod https://code.kimi.com/kimi-code/install.ps1 | Invoke-Expression',
129
+ ].join('; ')
130
+ : undefined,
131
+ };
132
+ installers.zcode = {
133
+ supported: false,
134
+ command: '打开 ZCode 官方下载页,选择当前系统安装包',
135
+ packageManager: 'ZCode 官方安装包',
136
+ docsUrl: TOOL_DOCS_URLS.zcode,
137
+ note: 'ZCode 是桌面应用,官方提供 Windows、macOS 和 Linux 安装包,没有官方 npm CLI。',
138
+ };
107
139
  return installers;
108
140
  }
109
141
 
@@ -224,7 +256,115 @@ async function launchOpenCode(dirPath) {
224
256
  return launchInTerminal(dirPath, 'opencode');
225
257
  }
226
258
 
259
+ async function launchKimiCode(dirPath) {
260
+ if (process.platform === 'win32') {
261
+ const executable = await findKimiExecutable();
262
+ if (!executable) throw new Error('未检测到 Kimi Code,请先安装 kimi CLI');
263
+ return spawnDetached('cmd.exe', ['/c', 'start', '""', executable], { cwd: dirPath });
264
+ }
265
+ return launchInTerminal(dirPath, 'kimi');
266
+ }
267
+
268
+ async function findKimiExecutable() {
269
+ if (process.platform !== 'win32') return commandExists('kimi') ? 'kimi' : null;
270
+
271
+ const candidates = [
272
+ path.join(process.env.USERPROFILE || '', '.kimi-code', 'bin', 'kimi.exe'),
273
+ ];
274
+ const registryPath = spawnSync('reg.exe', ['query', 'HKCU\\Environment', '/v', 'Path'], {
275
+ encoding: 'utf8',
276
+ windowsHide: true,
277
+ }).stdout || '';
278
+ const pathLine = registryPath.split(/\r?\n/).find(line => /\sPath\s+REG_(?:EXPAND_)?SZ\s+/i.test(line));
279
+ if (pathLine) {
280
+ const userPath = pathLine.replace(/^.*?REG_(?:EXPAND_)?SZ\s+/i, '').trim();
281
+ for (const directory of userPath.split(';').map(value => value.trim()).filter(Boolean)) {
282
+ candidates.push(path.join(directory, 'kimi.exe'));
283
+ }
284
+ }
285
+
286
+ for (const candidate of candidates) {
287
+ try {
288
+ const stat = await fs.stat(candidate);
289
+ if (stat.isFile()) return candidate;
290
+ } catch {}
291
+ }
292
+ return null;
293
+ }
294
+
295
+ async function launchZCode(dirPath) {
296
+ const executable = await findZCodeExecutable();
297
+ if (!executable) throw new Error('未检测到 ZCode,请从官网下载桌面安装包');
298
+ return spawnDetached(executable, [dirPath]);
299
+ }
300
+
301
+ async function findZCodeExecutable() {
302
+ const candidates = process.platform === 'win32'
303
+ ? [
304
+ path.join(process.env.LOCALAPPDATA || '', 'Programs', 'ZCode', 'ZCode.exe'),
305
+ path.join(process.env.LOCALAPPDATA || '', 'ZCode', 'ZCode.exe'),
306
+ path.join(process.env.PROGRAMFILES || '', 'ZCode', 'ZCode.exe'),
307
+ ]
308
+ : process.platform === 'darwin'
309
+ ? ['/Applications/ZCode.app/Contents/MacOS/ZCode']
310
+ : ['/usr/bin/zcode', '/usr/local/bin/zcode'];
311
+
312
+ if (process.platform === 'win32') {
313
+ const registryRoots = [
314
+ 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
315
+ 'HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
316
+ 'HKLM\\Software\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall',
317
+ ];
318
+ for (const root of registryRoots) {
319
+ const output = spawnSync('reg.exe', ['query', root, '/s'], {
320
+ encoding: 'utf8',
321
+ windowsHide: true,
322
+ }).stdout || '';
323
+ for (const line of output.split(/\r?\n/)) {
324
+ if (!/DisplayIcon|InstallLocation/i.test(line) || !/zcode/i.test(line)) continue;
325
+ const value = line.replace(/^.*?REG_SZ\s+/i, '').trim().replace(/^"|"$/g, '').replace(/,\d+$/, '');
326
+ if (value && !/\.(ico|png|jpg|jpeg)$/i.test(value)) candidates.push(value);
327
+ }
328
+ }
329
+ const running = spawnSync('powershell.exe', [
330
+ '-NoLogo', '-NoProfile', '-Command',
331
+ "Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -match 'ZCode' } | Select-Object -ExpandProperty Path",
332
+ ], { encoding: 'utf8', windowsHide: true }).stdout || '';
333
+ candidates.push(...running.split(/\r?\n/).map(value => value.trim()).filter(Boolean));
334
+ }
335
+
336
+ for (const candidate of candidates.filter(Boolean)) {
337
+ const normalized = String(candidate).trim().replace(/^"|"$/g, '').replace(/,\d+$/, '');
338
+ if (/\.(ico|png|jpg|jpeg)$/i.test(normalized)) continue;
339
+ try {
340
+ const stat = await fs.stat(normalized);
341
+ if (stat.isFile()) return normalized;
342
+ if (stat.isDirectory()) {
343
+ for (const name of ['ZCode.exe', 'zcode.exe']) {
344
+ const executable = path.join(normalized, name);
345
+ try {
346
+ const executableStat = await fs.stat(executable);
347
+ if (executableStat.isFile()) return executable;
348
+ } catch {}
349
+ }
350
+ }
351
+ } catch {}
352
+ }
353
+ return null;
354
+ }
355
+
227
356
  async function launchToolInstaller(installer, dirPath = process.cwd()) {
357
+ if (installer.kind === 'script') {
358
+ if (process.platform === 'win32') {
359
+ // 显式创建新控制台;编码脚本可避免 cmd.exe 截获 PowerShell 管道符。
360
+ const encoded = Buffer.from(installer.executionCommand || installer.command, 'utf16le').toString('base64');
361
+ return spawnDetached('cmd.exe', [
362
+ '/c', 'start', '""', 'powershell.exe', '-NoLogo', '-NoExit',
363
+ '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded,
364
+ ], { cwd: dirPath });
365
+ }
366
+ return launchInTerminal(dirPath, 'bash', ['-lc', installer.command]);
367
+ }
228
368
  if (process.platform === 'win32') {
229
369
  // 安装命令完全来自服务端白名单。使用可见 cmd 窗口,让用户看到进度和错误。
230
370
  const commandLine = [installer.executable, ...installer.args].join(' ');
@@ -516,6 +656,32 @@ export function registerFileOpenRoutes({
516
656
  }
517
657
  }));
518
658
 
659
+ // 用 Kimi Code 打开目录
660
+ app.post('/api/open-directory-with-kimi', asyncRoute(async (req, res) => {
661
+ const { path: dirPath } = req.body || {};
662
+ if (!dirPath) throw new HttpError(400, '目录路径不能为空');
663
+ try {
664
+ await fs.access(dirPath);
665
+ await launchKimiCode(dirPath);
666
+ res.json({ success: true, message: '已用 Kimi Code 打开目录' });
667
+ } catch (error) {
668
+ res.status(400).json({ success: false, error: error.message || '未检测到 Kimi Code,请先安装 kimi CLI' });
669
+ }
670
+ }));
671
+
672
+ // 用 ZCode 打开目录
673
+ app.post('/api/open-directory-with-zcode', asyncRoute(async (req, res) => {
674
+ const { path: dirPath } = req.body || {};
675
+ if (!dirPath) throw new HttpError(400, '目录路径不能为空');
676
+ try {
677
+ await fs.access(dirPath);
678
+ await launchZCode(dirPath);
679
+ res.json({ success: true, message: '已用 ZCode 打开目录' });
680
+ } catch (error) {
681
+ res.status(400).json({ success: false, error: error.message || '未检测到 ZCode,请从官网下载桌面安装包' });
682
+ }
683
+ }));
684
+
519
685
  // 安装本地工具:前端只能传固定 tool id,实际命令由服务端按平台从白名单选择。
520
686
  app.post('/api/install-tool', asyncRoute(async (req, res) => {
521
687
  const { tool } = req.body || {};
@@ -559,11 +725,13 @@ export function registerFileOpenRoutes({
559
725
  setTimeout(() => finish(false), 15000);
560
726
  });
561
727
 
562
- const [vscode, claude, codex, opencode] = await Promise.all([
728
+ const [vscode, claude, codex, opencode, kimiExecutable, zcodeExecutable] = await Promise.all([
563
729
  checkCmd('code'),
564
730
  checkCmd('claude'),
565
731
  checkCmd('codex'),
566
732
  checkCmd('opencode'),
733
+ findKimiExecutable(),
734
+ findZCodeExecutable(),
567
735
  ]);
568
736
 
569
737
  const installers = getToolInstallers();
@@ -575,6 +743,8 @@ export function registerFileOpenRoutes({
575
743
  claude,
576
744
  codex,
577
745
  opencode,
746
+ kimi: !!kimiExecutable,
747
+ zcode: !!zcodeExecutable,
578
748
  });
579
749
  }));
580
750
  }
@@ -20,7 +20,7 @@ import path from 'path';
20
20
  import open from 'open';
21
21
  import os from 'os';
22
22
  import { spawn, exec, execSync } from 'child_process';
23
- import { ensureWithinCwd } from '../utils/pathGuard.js';
23
+ import { ensureWithinCwd, normalizeProjectPath } from '../utils/pathGuard.js';
24
24
  import { asyncRoute, HttpError } from '../utils/asyncRoute.js';
25
25
  import { invalidateCurrentProjectKey, invalidateRawConfigCache } from '../../../config.js';
26
26
  import { invalidateCwdCache } from '../../../utils/index.js';
@@ -148,7 +148,7 @@ export function registerFsRoutes({
148
148
  invalidateCurrentProjectKey();
149
149
  invalidateRawConfigCache();
150
150
  invalidateCwdCache();
151
- const newDirectory = process.cwd();
151
+ const newDirectory = normalizeProjectPath(process.cwd());
152
152
 
153
153
  // 更新当前项目路径和房间ID
154
154
  const oldProjectPath = getCurrentProjectPath();