zen-gitsync 2.15.11 → 2.15.12

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 (69) hide show
  1. package/package.json +3 -3
  2. package/src/gitCommit.js +475 -475
  3. package/src/ui/public/assets/AppVersionBadge-BaXEI9Aq.js +2 -0
  4. package/src/ui/public/assets/BranchSelector-DJhEF0y9.js +1 -0
  5. package/src/ui/public/assets/CommitForm-BfpcK-bz.js +6 -0
  6. package/src/ui/public/assets/ConsoleView-D838O0O6.js +17 -0
  7. package/src/ui/public/assets/CustomCommandManager-BcCK46df.js +1 -0
  8. package/src/ui/public/assets/EditorView-DnnqYNGF.js +0 -0
  9. package/src/ui/public/assets/{FlowExecutionViewer-CJgoxffE.js → FlowExecutionViewer-CSlMmcDS.js} +1 -1
  10. package/src/ui/public/assets/FlowOrchestrationWorkspace-UgHpZ3FR.js +21 -0
  11. package/src/ui/public/assets/LogList-uUgskwwp.js +7 -0
  12. package/src/ui/public/assets/MindmapView-CCmFMiHR.js +1 -0
  13. package/src/ui/public/assets/MonitorView-BxoqeX70.js +1 -0
  14. package/src/ui/public/assets/ProjectStartupButton-C3AwivIB.js +1 -0
  15. package/src/ui/public/assets/{RemoteRepoCard-CKQguRWi.js → RemoteRepoCard-BbxeVpje.js} +1 -1
  16. package/src/ui/public/assets/SourceMapView-DKv-Y6t_.js +3 -0
  17. package/src/ui/public/assets/{SvgIcon-DOkE0Qie.js → SvgIcon-sMM8XFgm.js} +1 -1
  18. package/src/ui/public/assets/TemplateManager-Dzj8rXVw.js +1 -0
  19. package/src/ui/public/assets/UserInputNode-BU7Trzq-.js +1 -0
  20. package/src/ui/public/assets/WorkbenchView-DTgRoOkv.js +12 -0
  21. package/src/ui/public/assets/{_plugin-vue_export-helper-DvV-QrsM.js → _plugin-vue_export-helper-8ryIYVvs.js} +1 -1
  22. package/src/ui/public/assets/{configStore-BRR4SloR.js → configStore-D77uNk8X.js} +1 -1
  23. package/src/ui/public/assets/element-plus-D8SQiesr.js +17 -0
  24. package/src/ui/public/assets/element-plus-DB_sOyH_.css +1 -0
  25. package/src/ui/public/assets/{flow-mindmap-BMvnvRl8.js → flow-mindmap-BLa-kSA7.js} +1 -1
  26. package/src/ui/public/assets/index-CpFreGqX.js +23 -0
  27. package/src/ui/public/assets/{socket-io-C2w3MxRu.js → socket-io-B1foIbJS.js} +1 -1
  28. package/src/ui/public/assets/{vendor--LaaJk9m.js → vendor-B9orL8iU.js} +37 -37
  29. package/src/ui/public/assets/vendor-Dm_yd-By.css +1 -0
  30. package/src/ui/public/assets/{vue-flow-MtbKS8yD.js → vue-flow-BY7LODit.js} +1 -1
  31. package/src/ui/public/favicon.svg +75 -75
  32. package/src/ui/public/index.html +27 -27
  33. package/src/ui/public/logo.svg +74 -74
  34. package/src/ui/server/middleware/requestLogger.js +53 -53
  35. package/src/ui/server/routes/branchStatus.js +105 -105
  36. package/src/ui/server/routes/fileOpen.js +309 -309
  37. package/src/ui/server/routes/fs.js +791 -791
  38. package/src/ui/server/routes/git/diff.js +426 -426
  39. package/src/ui/server/routes/git/diffUtils.js +149 -149
  40. package/src/ui/server/routes/git/stash.js +550 -550
  41. package/src/ui/server/routes/git/tags.js +176 -176
  42. package/src/ui/server/routes/git.js +181 -181
  43. package/src/ui/server/routes/gitOps.js +1211 -1211
  44. package/src/ui/server/routes/process.js +86 -86
  45. package/src/ui/server/routes/status.js +67 -67
  46. package/src/ui/server/utils/createSavePortToFile.js +48 -48
  47. package/src/ui/server/utils/instanceRegistry.js +282 -282
  48. package/src/ui/server/utils/startServerOnAvailablePort.js +116 -116
  49. package/src/utils/index.js +1168 -1168
  50. package/src/ui/public/assets/AppVersionBadge-CEhrATQo.js +0 -2
  51. package/src/ui/public/assets/BranchSelector-xLl3O-4I.js +0 -1
  52. package/src/ui/public/assets/CommitForm--izGtr_x.js +0 -6
  53. package/src/ui/public/assets/ConsoleView-BLYfnXAy.js +0 -17
  54. package/src/ui/public/assets/CustomCommandManager-D8Qd3b9m.js +0 -1
  55. package/src/ui/public/assets/EditorView-DG8tqp3L.js +0 -0
  56. package/src/ui/public/assets/FlowOrchestrationWorkspace-BAIqeASp.js +0 -21
  57. package/src/ui/public/assets/LogList-DjuL8nWB.js +0 -7
  58. package/src/ui/public/assets/MindmapView-DPomtQsu.js +0 -1
  59. package/src/ui/public/assets/MonitorView-gZhz528u.js +0 -1
  60. package/src/ui/public/assets/ProjectStartupButton-C8nDEd8N.js +0 -1
  61. package/src/ui/public/assets/SourceMapView-DfSRy6lc.js +0 -3
  62. package/src/ui/public/assets/TemplateManager-BJZloJIO.js +0 -1
  63. package/src/ui/public/assets/UserInputNode-DpRNQzrz.js +0 -1
  64. package/src/ui/public/assets/WorkbenchView-vaZsec5Z.js +0 -12
  65. package/src/ui/public/assets/element-plus-CL7fBdM_.js +0 -24
  66. package/src/ui/public/assets/element-plus-Cu9Y2raX.css +0 -1
  67. package/src/ui/public/assets/index-BAFCDhj2.js +0 -23
  68. package/src/ui/public/assets/vendor-CCbwTgrE.css +0 -1
  69. package/src/ui/server/.claude/codediff.txt +0 -8
@@ -1,309 +1,309 @@
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 fs from 'fs/promises';
16
- import { asyncRoute, HttpError } from '../utils/asyncRoute.js';
17
- import path from 'path';
18
- import open from 'open';
19
- import { spawn } from 'child_process';
20
-
21
- function spawnDetached(command, args, options = {}) {
22
- return new Promise((resolve, reject) => {
23
- const child = spawn(command, args, {
24
- detached: true,
25
- stdio: 'ignore',
26
- ...options
27
- });
28
-
29
- child.on('error', reject);
30
- child.on('spawn', () => {
31
- child.unref();
32
- resolve('success');
33
- });
34
- });
35
- }
36
-
37
- async function launchClaudeCode(dirPath, { permissionMode } = {}) {
38
- // 透传可选的权限模式参数到 claude CLI(如 acceptEdits)
39
- // 注意:permissionMode 必须是一个 token 字符串,避免 shell 注入
40
- const SAFE_MODE = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
41
- const cliArgs = [];
42
- if (permissionMode && typeof permissionMode === 'string' && SAFE_MODE.test(permissionMode)) {
43
- cliArgs.push('--permission-mode', permissionMode);
44
- }
45
-
46
- if (process.platform === 'win32') {
47
- return spawnDetached('cmd.exe', ['/c', 'start', '""', 'claude', ...cliArgs], {
48
- cwd: dirPath
49
- });
50
- }
51
-
52
- return spawnDetached('claude', cliArgs, {
53
- cwd: dirPath
54
- });
55
- }
56
-
57
- export function registerFileOpenRoutes({
58
- app
59
- }) {
60
- // 打开文件
61
- app.post('/api/open-file', asyncRoute(async (req, res) => {
62
- try {
63
- const { filePath, context } = req.body;
64
-
65
- if (!filePath) {
66
- return res.status(400).json({
67
- success: false,
68
- error: '文件路径不能为空'
69
- });
70
- }
71
-
72
- let targetFilePath = filePath;
73
-
74
- // 根据上下文处理不同的文件打开方式
75
- switch (context) {
76
- case 'git-status':
77
- // Git状态:直接打开当前工作目录中的文件
78
- targetFilePath = path.resolve(process.cwd(), filePath);
79
- break;
80
-
81
- case 'commit-detail':
82
- // 提交详情:这里可以考虑创建临时文件显示该提交时的文件内容
83
- // 暂时先打开当前版本的文件
84
- targetFilePath = path.resolve(process.cwd(), filePath);
85
- break;
86
-
87
- case 'stash-detail':
88
- // Stash详情:同样暂时打开当前版本的文件
89
- targetFilePath = path.resolve(process.cwd(), filePath);
90
- break;
91
-
92
- default:
93
- targetFilePath = path.resolve(process.cwd(), filePath);
94
- }
95
-
96
- try {
97
- // 检查文件是否存在
98
- await fs.access(targetFilePath);
99
-
100
- // 使用系统默认程序打开文件
101
- await open(targetFilePath, { wait: false });
102
-
103
- res.json({
104
- success: true,
105
- message: `已打开文件: ${path.basename(targetFilePath)}`
106
- });
107
- } catch (error) {
108
- // 如果文件不存在,尝试在编辑器中创建新文件
109
- if (error.code === 'ENOENT') {
110
- try {
111
- await open(targetFilePath, { wait: false });
112
- res.json({
113
- success: true,
114
- message: `已在编辑器中打开文件: ${path.basename(targetFilePath)}`
115
- });
116
- } catch (openError) {
117
- res.status(400).json({
118
- success: false,
119
- error: `无法打开文件 "${path.basename(targetFilePath)}": ${openError.message}`
120
- });
121
- }
122
- } else {
123
- res.status(400).json({
124
- success: false,
125
- error: `无法访问文件 "${path.basename(targetFilePath)}": ${error.message}`
126
- });
127
- }
128
- }
129
- } catch (error) {
130
- res.status(500).json({
131
- success: false,
132
- error: error.message
133
- });
134
- }
135
- }));
136
-
137
- // 用VSCode打开文件
138
- app.post('/api/open-with-vscode', asyncRoute(async (req, res) => {
139
- try {
140
- const { filePath, context } = req.body;
141
-
142
- if (!filePath) {
143
- return res.status(400).json({
144
- success: false,
145
- error: '文件路径不能为空'
146
- });
147
- }
148
-
149
- let targetFilePath = filePath;
150
-
151
- // 根据上下文处理不同的文件打开方式
152
- switch (context) {
153
- case 'git-status':
154
- case 'commit-detail':
155
- case 'stash-detail':
156
- targetFilePath = path.resolve(process.cwd(), filePath);
157
- break;
158
- default:
159
- targetFilePath = path.resolve(process.cwd(), filePath);
160
- }
161
-
162
- try {
163
- // 使用VSCode打开文件
164
- // 尝试使用 'code' 命令打开文件
165
- // 使用已导入的 spawn
166
-
167
- // 创建一个Promise来处理spawn的异步结果
168
- const spawnPromise = new Promise((resolve, reject) => {
169
- const vscodeProcess = spawn('code', [targetFilePath], {
170
- detached: true,
171
- stdio: 'ignore'
172
- });
173
-
174
- // 监听错误事件
175
- vscodeProcess.on('error', (err) => {
176
- reject(err);
177
- });
178
-
179
- // 监听spawn事件,表示进程成功启动
180
- vscodeProcess.on('spawn', () => {
181
- resolve('success');
182
- });
183
-
184
- vscodeProcess.unref();
185
- });
186
-
187
- await spawnPromise;
188
-
189
- res.json({
190
- success: true,
191
- message: `已用VSCode打开文件: ${path.basename(targetFilePath)}`
192
- });
193
- } catch (error) {
194
- // 如果VSCode命令不可用,尝试直接用open打开
195
- try {
196
- await open(targetFilePath, { app: { name: 'code' } });
197
- res.json({
198
- success: true,
199
- message: `已用VSCode打开文件: ${path.basename(targetFilePath)}`
200
- });
201
- } catch (openError) {
202
- // 最后的备用方案:尝试用系统默认编辑器打开
203
- try {
204
- await open(targetFilePath);
205
- res.json({
206
- success: true,
207
- message: `VSCode不可用,已用系统默认程序打开文件: ${path.basename(targetFilePath)}`
208
- });
209
- } catch (finalError) {
210
- res.status(400).json({
211
- success: false,
212
- error: `无法打开文件 "${path.basename(targetFilePath)}": VSCode可能未安装或未添加到PATH,且系统默认程序也无法打开该文件`
213
- });
214
- }
215
- }
216
- }
217
- } catch (error) {
218
- res.status(500).json({
219
- success: false,
220
- error: error.message
221
- });
222
- }
223
- }));
224
-
225
- // 用 VSCode 打开目录
226
- app.post('/api/open-directory-with-vscode', asyncRoute(async (req, res) => {
227
- try {
228
- const { path: dirPath } = req.body;
229
- if (!dirPath) {
230
- throw new HttpError(400, '目录路径不能为空');
231
- }
232
-
233
- try {
234
- await spawnDetached('code', [dirPath]);
235
- res.json({ success: true, message: '已用 VSCode 打开目录' });
236
- } catch {
237
- // fallback:通过 open 模块指定 code 应用
238
- try {
239
- await open(dirPath, { app: { name: 'code' } });
240
- res.json({ success: true, message: '已用 VSCode 打开目录' });
241
- } catch (openError) {
242
- res.status(400).json({ success: false, error: 'VSCode 可能未安装或未添加到 PATH' });
243
- }
244
- }
245
- } catch (error) {
246
- res.status(500).json({ success: false, error: error.message });
247
- }
248
- }));
249
-
250
- // 用 Claude Code 打开目录
251
- app.post('/api/open-directory-with-claude-code', asyncRoute(async (req, res) => {
252
- try {
253
- const { path: dirPath, permissionMode } = req.body || {};
254
- if (!dirPath) {
255
- throw new HttpError(400, '目录路径不能为空');
256
- }
257
-
258
- try {
259
- await fs.access(dirPath);
260
- } catch (error) {
261
- throw new HttpError(400, `目录不存在或不可访问: ${dirPath}`);
262
- }
263
-
264
- try {
265
- await launchClaudeCode(dirPath, { permissionMode });
266
- const message = permissionMode
267
- ? `已用 Claude Code 打开目录(permission-mode=${permissionMode})`
268
- : '已用 Claude Code 打开目录';
269
- res.json({ success: true, message });
270
- } catch (error) {
271
- res.status(400).json({
272
- success: false,
273
- error: '未检测到 Claude Code,请先安装并确保可以在终端中直接运行 claude'
274
- });
275
- }
276
- } catch (error) {
277
- res.status(500).json({ success: false, error: error.message });
278
- }
279
- }));
280
-
281
- // 检测本地工具是否已安装(供前端根据结果决定是否显示对应按钮)
282
- // 检测方式: spawn 'tool --version',exit 0 即视为已安装
283
- // 超时 3s 避免某个工具卡在 PATH 解析上时整个接口挂起
284
- app.get('/api/check-tools', asyncRoute(async (req, res) => {
285
- const checkCmd = (cmd) => new Promise((resolve) => {
286
- const child = spawn(cmd, ['--version'], {
287
- stdio: 'ignore',
288
- shell: process.platform === 'win32',
289
- });
290
- let done = false;
291
- const finish = (ok) => {
292
- if (done) return;
293
- done = true;
294
- try { child.kill('SIGKILL'); } catch {}
295
- resolve(ok);
296
- };
297
- child.on('error', () => finish(false));
298
- child.on('exit', (code) => finish(code === 0));
299
- setTimeout(() => finish(false), 3000);
300
- });
301
-
302
- const [vscode, claude] = await Promise.all([
303
- checkCmd('code'),
304
- checkCmd('claude'),
305
- ]);
306
-
307
- res.json({ success: true, vscode, claude });
308
- }));
309
- }
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 fs from 'fs/promises';
16
+ import { asyncRoute, HttpError } from '../utils/asyncRoute.js';
17
+ import path from 'path';
18
+ import open from 'open';
19
+ import { spawn } from 'child_process';
20
+
21
+ function spawnDetached(command, args, options = {}) {
22
+ return new Promise((resolve, reject) => {
23
+ const child = spawn(command, args, {
24
+ detached: true,
25
+ stdio: 'ignore',
26
+ ...options
27
+ });
28
+
29
+ child.on('error', reject);
30
+ child.on('spawn', () => {
31
+ child.unref();
32
+ resolve('success');
33
+ });
34
+ });
35
+ }
36
+
37
+ async function launchClaudeCode(dirPath, { permissionMode } = {}) {
38
+ // 透传可选的权限模式参数到 claude CLI(如 acceptEdits)
39
+ // 注意:permissionMode 必须是一个 token 字符串,避免 shell 注入
40
+ const SAFE_MODE = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
41
+ const cliArgs = [];
42
+ if (permissionMode && typeof permissionMode === 'string' && SAFE_MODE.test(permissionMode)) {
43
+ cliArgs.push('--permission-mode', permissionMode);
44
+ }
45
+
46
+ if (process.platform === 'win32') {
47
+ return spawnDetached('cmd.exe', ['/c', 'start', '""', 'claude', ...cliArgs], {
48
+ cwd: dirPath
49
+ });
50
+ }
51
+
52
+ return spawnDetached('claude', cliArgs, {
53
+ cwd: dirPath
54
+ });
55
+ }
56
+
57
+ export function registerFileOpenRoutes({
58
+ app
59
+ }) {
60
+ // 打开文件
61
+ app.post('/api/open-file', asyncRoute(async (req, res) => {
62
+ try {
63
+ const { filePath, context } = req.body;
64
+
65
+ if (!filePath) {
66
+ return res.status(400).json({
67
+ success: false,
68
+ error: '文件路径不能为空'
69
+ });
70
+ }
71
+
72
+ let targetFilePath = filePath;
73
+
74
+ // 根据上下文处理不同的文件打开方式
75
+ switch (context) {
76
+ case 'git-status':
77
+ // Git状态:直接打开当前工作目录中的文件
78
+ targetFilePath = path.resolve(process.cwd(), filePath);
79
+ break;
80
+
81
+ case 'commit-detail':
82
+ // 提交详情:这里可以考虑创建临时文件显示该提交时的文件内容
83
+ // 暂时先打开当前版本的文件
84
+ targetFilePath = path.resolve(process.cwd(), filePath);
85
+ break;
86
+
87
+ case 'stash-detail':
88
+ // Stash详情:同样暂时打开当前版本的文件
89
+ targetFilePath = path.resolve(process.cwd(), filePath);
90
+ break;
91
+
92
+ default:
93
+ targetFilePath = path.resolve(process.cwd(), filePath);
94
+ }
95
+
96
+ try {
97
+ // 检查文件是否存在
98
+ await fs.access(targetFilePath);
99
+
100
+ // 使用系统默认程序打开文件
101
+ await open(targetFilePath, { wait: false });
102
+
103
+ res.json({
104
+ success: true,
105
+ message: `已打开文件: ${path.basename(targetFilePath)}`
106
+ });
107
+ } catch (error) {
108
+ // 如果文件不存在,尝试在编辑器中创建新文件
109
+ if (error.code === 'ENOENT') {
110
+ try {
111
+ await open(targetFilePath, { wait: false });
112
+ res.json({
113
+ success: true,
114
+ message: `已在编辑器中打开文件: ${path.basename(targetFilePath)}`
115
+ });
116
+ } catch (openError) {
117
+ res.status(400).json({
118
+ success: false,
119
+ error: `无法打开文件 "${path.basename(targetFilePath)}": ${openError.message}`
120
+ });
121
+ }
122
+ } else {
123
+ res.status(400).json({
124
+ success: false,
125
+ error: `无法访问文件 "${path.basename(targetFilePath)}": ${error.message}`
126
+ });
127
+ }
128
+ }
129
+ } catch (error) {
130
+ res.status(500).json({
131
+ success: false,
132
+ error: error.message
133
+ });
134
+ }
135
+ }));
136
+
137
+ // 用VSCode打开文件
138
+ app.post('/api/open-with-vscode', asyncRoute(async (req, res) => {
139
+ try {
140
+ const { filePath, context } = req.body;
141
+
142
+ if (!filePath) {
143
+ return res.status(400).json({
144
+ success: false,
145
+ error: '文件路径不能为空'
146
+ });
147
+ }
148
+
149
+ let targetFilePath = filePath;
150
+
151
+ // 根据上下文处理不同的文件打开方式
152
+ switch (context) {
153
+ case 'git-status':
154
+ case 'commit-detail':
155
+ case 'stash-detail':
156
+ targetFilePath = path.resolve(process.cwd(), filePath);
157
+ break;
158
+ default:
159
+ targetFilePath = path.resolve(process.cwd(), filePath);
160
+ }
161
+
162
+ try {
163
+ // 使用VSCode打开文件
164
+ // 尝试使用 'code' 命令打开文件
165
+ // 使用已导入的 spawn
166
+
167
+ // 创建一个Promise来处理spawn的异步结果
168
+ const spawnPromise = new Promise((resolve, reject) => {
169
+ const vscodeProcess = spawn('code', [targetFilePath], {
170
+ detached: true,
171
+ stdio: 'ignore'
172
+ });
173
+
174
+ // 监听错误事件
175
+ vscodeProcess.on('error', (err) => {
176
+ reject(err);
177
+ });
178
+
179
+ // 监听spawn事件,表示进程成功启动
180
+ vscodeProcess.on('spawn', () => {
181
+ resolve('success');
182
+ });
183
+
184
+ vscodeProcess.unref();
185
+ });
186
+
187
+ await spawnPromise;
188
+
189
+ res.json({
190
+ success: true,
191
+ message: `已用VSCode打开文件: ${path.basename(targetFilePath)}`
192
+ });
193
+ } catch (error) {
194
+ // 如果VSCode命令不可用,尝试直接用open打开
195
+ try {
196
+ await open(targetFilePath, { app: { name: 'code' } });
197
+ res.json({
198
+ success: true,
199
+ message: `已用VSCode打开文件: ${path.basename(targetFilePath)}`
200
+ });
201
+ } catch (openError) {
202
+ // 最后的备用方案:尝试用系统默认编辑器打开
203
+ try {
204
+ await open(targetFilePath);
205
+ res.json({
206
+ success: true,
207
+ message: `VSCode不可用,已用系统默认程序打开文件: ${path.basename(targetFilePath)}`
208
+ });
209
+ } catch (finalError) {
210
+ res.status(400).json({
211
+ success: false,
212
+ error: `无法打开文件 "${path.basename(targetFilePath)}": VSCode可能未安装或未添加到PATH,且系统默认程序也无法打开该文件`
213
+ });
214
+ }
215
+ }
216
+ }
217
+ } catch (error) {
218
+ res.status(500).json({
219
+ success: false,
220
+ error: error.message
221
+ });
222
+ }
223
+ }));
224
+
225
+ // 用 VSCode 打开目录
226
+ app.post('/api/open-directory-with-vscode', asyncRoute(async (req, res) => {
227
+ try {
228
+ const { path: dirPath } = req.body;
229
+ if (!dirPath) {
230
+ throw new HttpError(400, '目录路径不能为空');
231
+ }
232
+
233
+ try {
234
+ await spawnDetached('code', [dirPath]);
235
+ res.json({ success: true, message: '已用 VSCode 打开目录' });
236
+ } catch {
237
+ // fallback:通过 open 模块指定 code 应用
238
+ try {
239
+ await open(dirPath, { app: { name: 'code' } });
240
+ res.json({ success: true, message: '已用 VSCode 打开目录' });
241
+ } catch (openError) {
242
+ res.status(400).json({ success: false, error: 'VSCode 可能未安装或未添加到 PATH' });
243
+ }
244
+ }
245
+ } catch (error) {
246
+ res.status(500).json({ success: false, error: error.message });
247
+ }
248
+ }));
249
+
250
+ // 用 Claude Code 打开目录
251
+ app.post('/api/open-directory-with-claude-code', asyncRoute(async (req, res) => {
252
+ try {
253
+ const { path: dirPath, permissionMode } = req.body || {};
254
+ if (!dirPath) {
255
+ throw new HttpError(400, '目录路径不能为空');
256
+ }
257
+
258
+ try {
259
+ await fs.access(dirPath);
260
+ } catch (error) {
261
+ throw new HttpError(400, `目录不存在或不可访问: ${dirPath}`);
262
+ }
263
+
264
+ try {
265
+ await launchClaudeCode(dirPath, { permissionMode });
266
+ const message = permissionMode
267
+ ? `已用 Claude Code 打开目录(permission-mode=${permissionMode})`
268
+ : '已用 Claude Code 打开目录';
269
+ res.json({ success: true, message });
270
+ } catch (error) {
271
+ res.status(400).json({
272
+ success: false,
273
+ error: '未检测到 Claude Code,请先安装并确保可以在终端中直接运行 claude'
274
+ });
275
+ }
276
+ } catch (error) {
277
+ res.status(500).json({ success: false, error: error.message });
278
+ }
279
+ }));
280
+
281
+ // 检测本地工具是否已安装(供前端根据结果决定是否显示对应按钮)
282
+ // 检测方式: spawn 'tool --version',exit 0 即视为已安装
283
+ // 超时 3s 避免某个工具卡在 PATH 解析上时整个接口挂起
284
+ app.get('/api/check-tools', asyncRoute(async (req, res) => {
285
+ const checkCmd = (cmd) => new Promise((resolve) => {
286
+ const child = spawn(cmd, ['--version'], {
287
+ stdio: 'ignore',
288
+ shell: process.platform === 'win32',
289
+ });
290
+ let done = false;
291
+ const finish = (ok) => {
292
+ if (done) return;
293
+ done = true;
294
+ try { child.kill('SIGKILL'); } catch {}
295
+ resolve(ok);
296
+ };
297
+ child.on('error', () => finish(false));
298
+ child.on('exit', (code) => finish(code === 0));
299
+ setTimeout(() => finish(false), 3000);
300
+ });
301
+
302
+ const [vscode, claude] = await Promise.all([
303
+ checkCmd('code'),
304
+ checkCmd('claude'),
305
+ ]);
306
+
307
+ res.json({ success: true, vscode, claude });
308
+ }));
309
+ }