zen-gitsync 2.12.2 → 2.12.3
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.
- package/LICENSE +190 -21
- package/index.js +25 -11
- package/package.json +2 -2
- package/scripts/convert-colors-to-vars.cjs +286 -272
- package/scripts/convert-fontsize-to-vars.cjs +221 -207
- package/scripts/convert-spacing-to-vars.cjs +256 -242
- package/scripts/convert-to-standard-vars.cjs +282 -268
- package/scripts/release.js +599 -585
- package/src/config.js +350 -336
- package/src/gitCommit.js +455 -440
- package/src/ui/public/assets/EditorView-CbqSI9nw.css +1 -0
- package/src/ui/public/assets/{EditorView-bnJmBq-i.js → EditorView-GS5cmh99.js} +2 -2
- package/src/ui/public/assets/SourceMapView-DyMK80hS.css +1 -0
- package/src/ui/public/assets/{SourceMapView-Rz5SD0A0.js → SourceMapView-_YRtzmZZ.js} +3 -3
- package/src/ui/public/assets/{index-bOs5P8fz.css → index-ML5Y-5lO.css} +1 -1
- package/src/ui/public/assets/{index-Bo3tntQh.js → index-yky0Sd13.js} +11 -11
- package/src/ui/public/index.html +2 -2
- package/src/ui/server/index.js +410 -396
- package/src/ui/server/middleware/requestLogger.js +51 -37
- package/src/ui/server/routes/branchStatus.js +101 -87
- package/src/ui/server/routes/code.js +110 -96
- package/src/ui/server/routes/codeAnalysis.js +995 -981
- package/src/ui/server/routes/config.js +1172 -1158
- package/src/ui/server/routes/exec.js +272 -258
- package/src/ui/server/routes/fileOpen.js +279 -265
- package/src/ui/server/routes/fs.js +701 -687
- package/src/ui/server/routes/git/diff.js +352 -338
- package/src/ui/server/routes/git/diffUtils.js +128 -114
- package/src/ui/server/routes/git/stash.js +552 -538
- package/src/ui/server/routes/git/tags.js +172 -158
- package/src/ui/server/routes/git.js +190 -176
- package/src/ui/server/routes/gitOps.js +1179 -1165
- package/src/ui/server/routes/instances.js +14 -0
- package/src/ui/server/routes/npm.js +1023 -1009
- package/src/ui/server/routes/process.js +82 -68
- package/src/ui/server/routes/status.js +67 -53
- package/src/ui/server/routes/terminal.js +319 -305
- package/src/ui/server/socket/registerUiSocketHandlers.js +226 -212
- package/src/ui/server/utils/createSavePortToFile.js +46 -32
- package/src/ui/server/utils/instanceRegistry.js +14 -0
- package/src/ui/server/utils/pathGuard.js +14 -0
- package/src/ui/server/utils/pathGuard.test.js +14 -0
- package/src/ui/server/utils/randomStartPort.js +14 -0
- package/src/ui/server/utils/startServerOnAvailablePort.js +101 -87
- package/src/utils/index.js +14 -0
- package/src/ui/public/assets/EditorView-CHBjgiZc.css +0 -1
- package/src/ui/public/assets/SourceMapView-DhQX0K7t.css +0 -1
package/src/ui/server/index.js
CHANGED
|
@@ -1,396 +1,410 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
import
|
|
23
|
-
import
|
|
24
|
-
import
|
|
25
|
-
import
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
let
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
app,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
recentPushStatus =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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 express from 'express';
|
|
16
|
+
import { createServer } from 'http';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import { execGitCommand, getCommandHistory, addCommandToHistory, clearCommandHistory, registerSocketIO, execGitAddWithLockFilter, checkAndClearGitLock } from '../../utils/index.js';
|
|
20
|
+
import open from 'open';
|
|
21
|
+
import config from '../../config.js';
|
|
22
|
+
import chalk from 'chalk';
|
|
23
|
+
import fs from 'fs/promises';
|
|
24
|
+
import fsSync from 'fs';
|
|
25
|
+
import os from 'os';
|
|
26
|
+
import { Server } from 'socket.io';
|
|
27
|
+
import { spawn, exec } from 'child_process';
|
|
28
|
+
import iconv from 'iconv-lite';
|
|
29
|
+
import { createRequestLogger } from './middleware/requestLogger.js';
|
|
30
|
+
import { registerUiSocketHandlers } from './socket/registerUiSocketHandlers.js';
|
|
31
|
+
import { registerExecRoutes } from './routes/exec.js';
|
|
32
|
+
import { registerTerminalRoutes } from './routes/terminal.js';
|
|
33
|
+
import { registerProcessRoutes } from './routes/process.js';
|
|
34
|
+
import { registerStatusRoutes } from './routes/status.js';
|
|
35
|
+
import { registerBranchStatusRoutes } from './routes/branchStatus.js';
|
|
36
|
+
import { registerConfigRoutes } from './routes/config.js';
|
|
37
|
+
import { registerGitRoutes } from './routes/git.js';
|
|
38
|
+
import { registerFsRoutes } from './routes/fs.js';
|
|
39
|
+
import { registerNpmRoutes } from './routes/npm.js';
|
|
40
|
+
import { registerFileOpenRoutes } from './routes/fileOpen.js';
|
|
41
|
+
import { registerGitOpsRoutes } from './routes/gitOps.js';
|
|
42
|
+
import { registerCodeRoutes } from './routes/code.js';
|
|
43
|
+
import { registerCodeAnalysisRoutes } from './routes/codeAnalysis.js';
|
|
44
|
+
import { registerInstancesRoutes } from './routes/instances.js';
|
|
45
|
+
import { createInstanceRegistry } from './utils/instanceRegistry.js';
|
|
46
|
+
import { createSavePortToFile } from './utils/createSavePortToFile.js';
|
|
47
|
+
import { startServerOnAvailablePort } from './utils/startServerOnAvailablePort.js';
|
|
48
|
+
import { resolveStartPort } from './utils/randomStartPort.js';
|
|
49
|
+
import { createFilePickerMiddleware } from 'local-file-picker';
|
|
50
|
+
import { createAiModelMiddleware } from 'ai-model-form';
|
|
51
|
+
|
|
52
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
53
|
+
const __dirname = path.dirname(__filename);
|
|
54
|
+
const configManager = config; // 确保 configManager 可用
|
|
55
|
+
// 存储正在运行的进程(用于停止功能)
|
|
56
|
+
const runningProcesses = new Map(); // key: processId, value: { childProcess, command, startTime }
|
|
57
|
+
let processIdCounter = 0;
|
|
58
|
+
|
|
59
|
+
const terminalSessions = new Map(); // key: terminalSessionId, value: { id, command, workingDirectory, pid, createdAt, lastStartedAt }
|
|
60
|
+
let terminalSessionIdCounter = 0;
|
|
61
|
+
|
|
62
|
+
// 分支状态缓存
|
|
63
|
+
let branchStatusCache = {
|
|
64
|
+
currentBranch: null,
|
|
65
|
+
upstreamBranch: null,
|
|
66
|
+
lastUpdate: 0,
|
|
67
|
+
cacheTimeout: 5000 // 5秒缓存
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// 推送状态标记 - 用于优化推送后的分支状态查询
|
|
71
|
+
let recentPushStatus = {
|
|
72
|
+
justPushed: false,
|
|
73
|
+
pushTime: 0,
|
|
74
|
+
validDuration: 10000 // 推送后10秒内认为分支状态是同步的
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const showConsole = true;
|
|
78
|
+
async function startUIServer(noOpen = false, savePort = false) {
|
|
79
|
+
const app = express();
|
|
80
|
+
const httpServer = createServer(app);
|
|
81
|
+
const io = new Server(httpServer);
|
|
82
|
+
if (showConsole) console.log(`创建服务成功`)
|
|
83
|
+
|
|
84
|
+
// 获取当前项目的唯一标识(使用工作目录路径)
|
|
85
|
+
// 需要在切换目录时更新,故使用 let
|
|
86
|
+
let currentProjectPath = process.cwd();
|
|
87
|
+
let projectRoomId = `project:${currentProjectPath.replace(/[\\/:\s]/g, '_')}`;
|
|
88
|
+
|
|
89
|
+
console.log(chalk.blue(`项目房间ID: ${projectRoomId}`));
|
|
90
|
+
console.log(chalk.blue(`项目路径: ${currentProjectPath}`));
|
|
91
|
+
|
|
92
|
+
// 注册Socket.io实例,用于命令历史通知
|
|
93
|
+
registerSocketIO(io);
|
|
94
|
+
|
|
95
|
+
// 构建实例注册表(用于跨进程共享"当前运行中的 GUI"信息)
|
|
96
|
+
// 注册表文件位于用户主目录,所有 g ui 进程共享写入
|
|
97
|
+
const instanceRegistry = createInstanceRegistry({
|
|
98
|
+
fs,
|
|
99
|
+
path,
|
|
100
|
+
os,
|
|
101
|
+
registryPath: path.join(os.homedir(), '.zen-gitsync-instances.json')
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// 添加全局中间件来解析JSON请求体
|
|
105
|
+
app.use(express.json());
|
|
106
|
+
|
|
107
|
+
// 记录最近打开的目录(优先 Git 根目录,其次当前工作目录)
|
|
108
|
+
try {
|
|
109
|
+
let dirPath = process.cwd();
|
|
110
|
+
try {
|
|
111
|
+
if(showConsole) console.log(`记录最近打开目录`)
|
|
112
|
+
const { stdout } = await execGitCommand('git rev-parse --show-toplevel');
|
|
113
|
+
const root = stdout?.trim();
|
|
114
|
+
if (root) dirPath = root;
|
|
115
|
+
} catch (_) {
|
|
116
|
+
// 非Git仓库或命令失败,使用 CWD 即可
|
|
117
|
+
}
|
|
118
|
+
if (showConsole) console.log(`记录最近打开目录: ${dirPath}`)
|
|
119
|
+
await configManager.saveRecentDirectory(dirPath);
|
|
120
|
+
if (showConsole) console.log(chalk.gray(`已记录最近打开目录: ${dirPath}`));
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.warn(chalk.yellow(`记录最近目录失败: ${e?.message || e}`));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 添加请求日志中间件
|
|
126
|
+
app.use(createRequestLogger({ chalk }));
|
|
127
|
+
|
|
128
|
+
// 静态文件服务
|
|
129
|
+
app.use(express.static(path.join(__dirname, '../public')));
|
|
130
|
+
|
|
131
|
+
registerExecRoutes({
|
|
132
|
+
app,
|
|
133
|
+
execGitCommand,
|
|
134
|
+
addCommandToHistory,
|
|
135
|
+
getCurrentProjectPath: () => currentProjectPath,
|
|
136
|
+
nextProcessId: () => processIdCounter++,
|
|
137
|
+
runningProcesses
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
registerCodeRoutes({
|
|
141
|
+
app
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
registerTerminalRoutes({
|
|
145
|
+
app,
|
|
146
|
+
getCurrentProjectPath: () => currentProjectPath,
|
|
147
|
+
nextTerminalSessionId: () => terminalSessionIdCounter++,
|
|
148
|
+
terminalSessions
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
registerProcessRoutes({
|
|
152
|
+
app,
|
|
153
|
+
runningProcesses,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// API路由
|
|
157
|
+
// 移除了 /api/status 端点,因为前端只使用 porcelain 格式
|
|
158
|
+
registerStatusRoutes({
|
|
159
|
+
app,
|
|
160
|
+
getCommandHistory,
|
|
161
|
+
execGitCommand
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
registerBranchStatusRoutes({
|
|
165
|
+
app,
|
|
166
|
+
execGitCommand,
|
|
167
|
+
getIsGitRepo: () => isGitRepo,
|
|
168
|
+
getBranchStatusCache: () => branchStatusCache,
|
|
169
|
+
setBranchStatusCache: (v) => { branchStatusCache = v; },
|
|
170
|
+
getRecentPushStatus: () => recentPushStatus,
|
|
171
|
+
setRecentPushStatus: (v) => { recentPushStatus = v; }
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// 清除分支缓存的函数(在分支切换时调用)
|
|
175
|
+
function clearBranchCache() {
|
|
176
|
+
console.log('清除分支缓存');
|
|
177
|
+
// 清除5秒分支状态缓存
|
|
178
|
+
branchStatusCache = {
|
|
179
|
+
currentBranch: null,
|
|
180
|
+
upstreamBranch: null,
|
|
181
|
+
lastUpdate: 0,
|
|
182
|
+
cacheTimeout: 5000
|
|
183
|
+
};
|
|
184
|
+
// 清除推送状态标记
|
|
185
|
+
recentPushStatus = {
|
|
186
|
+
justPushed: false,
|
|
187
|
+
pushTime: 0,
|
|
188
|
+
validDuration: 10000
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
registerGitRoutes({
|
|
193
|
+
app,
|
|
194
|
+
execGitCommand,
|
|
195
|
+
clearBranchCache
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
registerFsRoutes({
|
|
199
|
+
app,
|
|
200
|
+
execGitCommand,
|
|
201
|
+
configManager,
|
|
202
|
+
io,
|
|
203
|
+
getCurrentProjectPath: () => currentProjectPath,
|
|
204
|
+
setCurrentProjectPath: (v) => { currentProjectPath = v; },
|
|
205
|
+
getProjectRoomId: () => projectRoomId,
|
|
206
|
+
setProjectRoomId: (v) => { projectRoomId = v; },
|
|
207
|
+
setIsGitRepo: (v) => { isGitRepo = v; }
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
registerConfigRoutes({
|
|
211
|
+
app,
|
|
212
|
+
configManager,
|
|
213
|
+
execGitCommand,
|
|
214
|
+
getCurrentProjectPath: () => currentProjectPath
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
registerNpmRoutes({
|
|
218
|
+
app,
|
|
219
|
+
getCurrentProjectPath: () => currentProjectPath
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
registerFileOpenRoutes({
|
|
223
|
+
app
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// local-file-picker 中间件,提供 /api/fs/* 文件浏览路由
|
|
227
|
+
app.use('/api', createFilePickerMiddleware());
|
|
228
|
+
|
|
229
|
+
// ai-model-form 中间件:提供 /api/ai-model/* 路由(模型列表/测试/暂存保存)
|
|
230
|
+
app.use('/api', createAiModelMiddleware());
|
|
231
|
+
|
|
232
|
+
registerCodeAnalysisRoutes({ app, configManager });
|
|
233
|
+
|
|
234
|
+
// 实例注册表 API:列出当前所有运行中的 GUI
|
|
235
|
+
registerInstancesRoutes({
|
|
236
|
+
app,
|
|
237
|
+
registry: instanceRegistry,
|
|
238
|
+
getCurrentInstanceId: () => process.pid
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
registerGitOpsRoutes({
|
|
242
|
+
app,
|
|
243
|
+
execGitCommand,
|
|
244
|
+
configManager,
|
|
245
|
+
execGitAddWithLockFilter,
|
|
246
|
+
addCommandToHistory,
|
|
247
|
+
clearCommandHistory,
|
|
248
|
+
checkAndClearGitLock,
|
|
249
|
+
getIsGitRepo: () => isGitRepo,
|
|
250
|
+
setRecentPushStatus: (v) => { recentPushStatus = v; }
|
|
251
|
+
});
|
|
252
|
+
registerUiSocketHandlers({
|
|
253
|
+
io,
|
|
254
|
+
getProjectRoomId: () => projectRoomId,
|
|
255
|
+
getCurrentProjectPath: () => currentProjectPath,
|
|
256
|
+
getAndBroadcastStatus,
|
|
257
|
+
getCommandHistory,
|
|
258
|
+
clearCommandHistory,
|
|
259
|
+
addCommandToHistory,
|
|
260
|
+
runningProcesses,
|
|
261
|
+
nextProcessId: () => ++processIdCounter,
|
|
262
|
+
spawn,
|
|
263
|
+
exec,
|
|
264
|
+
path,
|
|
265
|
+
iconv
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// 获取并广播Git状态 (优化版本 - 只获取porcelain格式)
|
|
269
|
+
async function getAndBroadcastStatus() {
|
|
270
|
+
try {
|
|
271
|
+
// 如果不是Git仓库,发送特殊状态
|
|
272
|
+
if (!isGitRepo) {
|
|
273
|
+
io.to(projectRoomId).emit('git_status_update', {
|
|
274
|
+
isGitRepo: false,
|
|
275
|
+
porcelain: '',
|
|
276
|
+
timestamp: new Date().toISOString(),
|
|
277
|
+
projectPath: currentProjectPath
|
|
278
|
+
});
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 只获取porcelain格式状态,不再获取完整的git status
|
|
283
|
+
const { stdout: porcelainOutput } = await execGitCommand('git status --porcelain --untracked-files=all');
|
|
284
|
+
|
|
285
|
+
// 广播到当前项目房间的所有客户端
|
|
286
|
+
io.to(projectRoomId).emit('git_status_update', {
|
|
287
|
+
isGitRepo: true,
|
|
288
|
+
porcelain: porcelainOutput,
|
|
289
|
+
timestamp: new Date().toISOString(),
|
|
290
|
+
projectPath: currentProjectPath
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
console.log(`已广播Git状态更新到房间: ${projectRoomId}`);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error('获取或广播Git状态失败:', error);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 检查当前目录是否是Git仓库
|
|
300
|
+
let isGitRepo = false;
|
|
301
|
+
try {
|
|
302
|
+
const { stdout } = await execGitCommand('git rev-parse --is-inside-work-tree', { log: false });
|
|
303
|
+
isGitRepo = stdout.trim() === 'true';
|
|
304
|
+
} catch (error) {
|
|
305
|
+
isGitRepo = false;
|
|
306
|
+
console.log(chalk.yellow('======================================'));
|
|
307
|
+
console.log(chalk.yellow(` 提示: 当前目录不是Git仓库`));
|
|
308
|
+
console.log(chalk.yellow(` 目录: ${process.cwd()}`));
|
|
309
|
+
console.log(chalk.yellow('======================================'));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 启动服务器
|
|
313
|
+
// 端口策略:默认从 [4000, 6000) 随机挑起点,再顺序扫描 EADDRINUSE;
|
|
314
|
+
// 可通过 PORT 环境变量强制使用固定端口(向后兼容 + 便于书签/调试)
|
|
315
|
+
const portStrategy = resolveStartPort();
|
|
316
|
+
if (portStrategy.source === 'env') {
|
|
317
|
+
console.log(chalk.cyan(`[端口] 使用环境变量 PORT=${portStrategy.startPort}`));
|
|
318
|
+
} else {
|
|
319
|
+
console.log(chalk.cyan(`[端口] 随机起点 ${portStrategy.startPort}(范围 ${portStrategy.min}-${portStrategy.max},遇到占用会顺延)`));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 创建一个函数来保存端口号到文件和环境变量
|
|
323
|
+
// 使用闭包保存端口状态,防止多次写入相同端口
|
|
324
|
+
const savePortToFile = createSavePortToFile({ savePort, fs, path });
|
|
325
|
+
// 使用变量标记回调是否已执行,防止多次触发
|
|
326
|
+
const callbackExecutedRef = { value: false };
|
|
327
|
+
|
|
328
|
+
// 用 'listening' 事件做注册触发:startServerOnAvailablePort 的 await
|
|
329
|
+
// 在端口重试场景下不一定按时返回,但 'listening' 事件只在服务器真正绑定端口时触发
|
|
330
|
+
let registerDone = false;
|
|
331
|
+
httpServer.once('listening', () => {
|
|
332
|
+
if (registerDone) return;
|
|
333
|
+
registerDone = true;
|
|
334
|
+
registerCurrentInstance().catch((e) => {
|
|
335
|
+
console.warn(chalk.yellow(`[instanceRegistry] 启动注册流程失败: ${e?.message || e}`));
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// 尝试在可用端口上启动服务器(不等待;listen 事件会驱动后续逻辑)
|
|
340
|
+
startServerOnAvailablePort({
|
|
341
|
+
httpServer,
|
|
342
|
+
startPort: portStrategy.startPort,
|
|
343
|
+
chalk,
|
|
344
|
+
open,
|
|
345
|
+
noOpen,
|
|
346
|
+
isGitRepo,
|
|
347
|
+
savePortToFile,
|
|
348
|
+
maxTries: 100,
|
|
349
|
+
callbackExecutedRef
|
|
350
|
+
}).catch((e) => {
|
|
351
|
+
console.error('启动服务器失败:', e);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// 把所有注册/心跳/watch 逻辑封装到独立函数,由 listening 事件触发
|
|
355
|
+
async function registerCurrentInstance() {
|
|
356
|
+
const addr = httpServer.address();
|
|
357
|
+
const currentPort = addr && addr.port;
|
|
358
|
+
if (!currentPort) {
|
|
359
|
+
console.warn(chalk.yellow('[instanceRegistry] 无法获取当前端口,跳过注册'));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
const projectName = await instanceRegistry._resolveProjectName(currentProjectPath);
|
|
365
|
+
await instanceRegistry.register({
|
|
366
|
+
pid: process.pid,
|
|
367
|
+
port: currentPort,
|
|
368
|
+
projectPath: currentProjectPath,
|
|
369
|
+
projectName,
|
|
370
|
+
hostname: os.hostname()
|
|
371
|
+
});
|
|
372
|
+
console.log(chalk.green(`[instanceRegistry] 已注册 pid=${process.pid} port=${currentPort} name=${projectName}`));
|
|
373
|
+
} catch (e) {
|
|
374
|
+
console.warn(chalk.yellow(`[instanceRegistry] 注册失败: ${e?.message || e}`));
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 5 秒心跳
|
|
379
|
+
const heartbeatTimer = setInterval(() => {
|
|
380
|
+
instanceRegistry.heartbeat(process.pid, { projectPath: currentProjectPath })
|
|
381
|
+
.catch((e) => console.warn(chalk.yellow(`[instanceRegistry] 心跳失败: ${e?.message || e}`)));
|
|
382
|
+
}, 5000);
|
|
383
|
+
|
|
384
|
+
// 监听注册表文件变化:任何进程写入都会触发,向本进程的所有客户端广播
|
|
385
|
+
// fs.watch 在 Windows 上偶有不可靠,Socket.IO 推送 + 前端轮询(15s)兜底
|
|
386
|
+
const stopWatcher = instanceRegistry.watch(async (fresh) => {
|
|
387
|
+
try {
|
|
388
|
+
io.emit('instances_changed', { instances: fresh });
|
|
389
|
+
} catch (e) {
|
|
390
|
+
console.warn(chalk.yellow(`[instanceRegistry] 广播失败: ${e?.message || e}`));
|
|
391
|
+
}
|
|
392
|
+
}, fsSync.watch);
|
|
393
|
+
|
|
394
|
+
// 优雅退出:SIGINT/SIGTERM 触发异步 unregister + 清心跳
|
|
395
|
+
const shutdown = async (signal) => {
|
|
396
|
+
try { clearInterval(heartbeatTimer); } catch (_) {}
|
|
397
|
+
try { await instanceRegistry.unregister(process.pid); } catch (_) {}
|
|
398
|
+
console.log(chalk.gray(`[instanceRegistry] 收到 ${signal},已清理本实例`));
|
|
399
|
+
};
|
|
400
|
+
process.on('SIGINT', () => { shutdown('SIGINT'); });
|
|
401
|
+
process.on('SIGTERM', () => { shutdown('SIGTERM'); });
|
|
402
|
+
// 'exit' 是同步钩子,无法 await:仅关闭 watcher;kill -9 走心跳超时
|
|
403
|
+
process.on('exit', () => {
|
|
404
|
+
try { stopWatcher && stopWatcher(); } catch (_) {}
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export default startUIServer;
|