zen-gitsync 2.9.8 → 2.9.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.
- package/README.md +100 -148
- package/package.json +1 -1
- package/src/ui/public/assets/{index-C-ZzwQYX.css → index-CzUoE1WP.css} +1 -1
- package/src/ui/public/assets/index-GE6lIBHK.js +79 -0
- package/src/ui/public/index.html +2 -2
- package/src/ui/server/index.js +143 -5311
- package/src/ui/server/index_pro.js +5483 -0
- package/src/ui/server/middleware/requestLogger.js +37 -0
- package/src/ui/server/routes/branchStatus.js +168 -0
- package/src/ui/server/routes/config.js +586 -0
- package/src/ui/server/routes/exec.js +247 -0
- package/src/ui/server/routes/fileOpen.js +173 -0
- package/src/ui/server/routes/fs.js +443 -0
- package/src/ui/server/routes/git/diff.js +206 -0
- package/src/ui/server/routes/git/diffUtils.js +114 -0
- package/src/ui/server/routes/git/stash.js +481 -0
- package/src/ui/server/routes/git/tags.js +158 -0
- package/src/ui/server/routes/git.js +176 -0
- package/src/ui/server/routes/gitOps.js +974 -0
- package/src/ui/server/routes/npm.js +981 -0
- package/src/ui/server/routes/process.js +68 -0
- package/src/ui/server/routes/status.js +24 -0
- package/src/ui/server/routes/terminal.js +244 -0
- package/src/ui/server/socket/registerUiSocketHandlers.js +212 -0
- package/src/ui/server/utils/createSavePortToFile.js +32 -0
- package/src/ui/server/utils/startServerOnAvailablePort.js +87 -0
- package/src/ui/public/assets/index-DXmNo3gw.js +0 -79
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export function registerProcessRoutes({
|
|
2
|
+
app,
|
|
3
|
+
runningProcesses,
|
|
4
|
+
exec
|
|
5
|
+
}) {
|
|
6
|
+
// 停止正在运行的进程
|
|
7
|
+
app.post('/api/kill-process', async (req, res) => {
|
|
8
|
+
try {
|
|
9
|
+
const { processId } = req.body || {};
|
|
10
|
+
if (!processId || typeof processId !== 'number') {
|
|
11
|
+
return res.status(400).json({ success: false, error: 'processId 必须是数字' });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const processInfo = runningProcesses.get(processId);
|
|
15
|
+
if (!processInfo) {
|
|
16
|
+
return res.status(404).json({
|
|
17
|
+
success: false,
|
|
18
|
+
error: `进程 #${processId} 不存在或已结束`
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(`[进程管理] 尝试停止进程 #${processId}: ${processInfo.command}`);
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
// 在 Windows 上需要使用 taskkill 来杀死整个进程树
|
|
26
|
+
if (process.platform === 'win32') {
|
|
27
|
+
// 使用已导入的 exec
|
|
28
|
+
// /F 强制终止, /T 终止进程树
|
|
29
|
+
exec(`taskkill /pid ${processInfo.childProcess.pid} /T /F`, (error) => {
|
|
30
|
+
if (error) {
|
|
31
|
+
console.error(`[进程管理] taskkill 失败:`, error);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
} else {
|
|
35
|
+
// Unix/Linux/Mac 使用 SIGTERM
|
|
36
|
+
processInfo.childProcess.kill('SIGTERM');
|
|
37
|
+
|
|
38
|
+
// 如果 2 秒后还没结束,使用 SIGKILL 强制终止
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
if (runningProcesses.has(processId)) {
|
|
41
|
+
console.log(`[进程管理] 进程 #${processId} 未响应 SIGTERM,使用 SIGKILL 强制终止`);
|
|
42
|
+
processInfo.childProcess.kill('SIGKILL');
|
|
43
|
+
}
|
|
44
|
+
}, 2000);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
res.json({
|
|
48
|
+
success: true,
|
|
49
|
+
message: `已发送停止信号到进程 #${processId}`,
|
|
50
|
+
processId,
|
|
51
|
+
command: processInfo.command
|
|
52
|
+
});
|
|
53
|
+
} catch (killError) {
|
|
54
|
+
console.error(`[进程管理] 停止进程失败:`, killError);
|
|
55
|
+
res.status(500).json({
|
|
56
|
+
success: false,
|
|
57
|
+
error: `停止进程失败: ${killError.message}`
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('停止进程接口失败:', error);
|
|
62
|
+
res.status(500).json({
|
|
63
|
+
success: false,
|
|
64
|
+
error: `停止进程失败: ${error.message}`
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function registerStatusRoutes({
|
|
2
|
+
app,
|
|
3
|
+
getCommandHistory,
|
|
4
|
+
execGitCommand
|
|
5
|
+
}) {
|
|
6
|
+
// Add new endpoint for command history
|
|
7
|
+
app.get('/api/command-history', async (req, res) => {
|
|
8
|
+
try {
|
|
9
|
+
const history = getCommandHistory();
|
|
10
|
+
res.json({ success: true, history });
|
|
11
|
+
} catch (error) {
|
|
12
|
+
res.status(500).json({ success: false, error: error.message });
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
app.get('/api/status_porcelain', async (req, res) => {
|
|
17
|
+
try {
|
|
18
|
+
const { stdout } = await execGitCommand('git status --porcelain --untracked-files=all');
|
|
19
|
+
res.json({ status: stdout });
|
|
20
|
+
} catch (error) {
|
|
21
|
+
res.status(500).json({ error: error.message });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { spawn, exec } from 'child_process';
|
|
2
|
+
|
|
3
|
+
export function registerTerminalRoutes({
|
|
4
|
+
app,
|
|
5
|
+
getCurrentProjectPath,
|
|
6
|
+
nextTerminalSessionId,
|
|
7
|
+
terminalSessions
|
|
8
|
+
}) {
|
|
9
|
+
async function startTerminalProcess({ command, workingDirectory }) {
|
|
10
|
+
const targetDir = workingDirectory || getCurrentProjectPath();
|
|
11
|
+
|
|
12
|
+
if (process.platform === 'win32') {
|
|
13
|
+
const cmdToRun = command.trim();
|
|
14
|
+
const safeWorkingDir = String(targetDir).replace(/"/g, '""');
|
|
15
|
+
const safeCmd = String(cmdToRun).replace(/"/g, '""');
|
|
16
|
+
|
|
17
|
+
const psScript = `$p = Start-Process -FilePath "cmd.exe" -ArgumentList "/K", "${safeCmd}" -WorkingDirectory "${safeWorkingDir}" -PassThru; Write-Output $p.Id`;
|
|
18
|
+
|
|
19
|
+
return await new Promise((resolve, reject) => {
|
|
20
|
+
const child = spawn('powershell.exe', ['-NoProfile', '-Command', psScript], {
|
|
21
|
+
windowsHide: true
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
let out = '';
|
|
25
|
+
let err = '';
|
|
26
|
+
|
|
27
|
+
child.stdout?.on('data', (d) => {
|
|
28
|
+
out += d.toString('utf8');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
child.stderr?.on('data', (d) => {
|
|
32
|
+
err += d.toString('utf8');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
child.on('error', (e) => reject(e));
|
|
36
|
+
|
|
37
|
+
child.on('close', (code) => {
|
|
38
|
+
if (code !== 0) {
|
|
39
|
+
return reject(new Error(err || `Start-Process 失败,退出码: ${code}`));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const pid = parseInt(String(out).trim(), 10);
|
|
43
|
+
if (!Number.isFinite(pid)) {
|
|
44
|
+
return reject(new Error(`无法获取终端 PID: ${out || err || 'unknown'}`));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
resolve({ pid });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (process.platform === 'darwin') {
|
|
53
|
+
const script = `tell application "Terminal" to do script "cd ${targetDir} && ${command.trim()}"`;
|
|
54
|
+
exec(`osascript -e '${script}'`, (error) => {
|
|
55
|
+
if (error) {
|
|
56
|
+
console.error('打开终端失败:', error);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
return { pid: null };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const terminalCommand = `gnome-terminal -- bash -c "cd ${targetDir} && ${command.trim()}; exec bash" || xterm -e "cd ${targetDir} && ${command.trim()}; bash"`;
|
|
63
|
+
exec(terminalCommand, (error) => {
|
|
64
|
+
if (error) {
|
|
65
|
+
console.error('打开终端失败:', error);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return { pid: null };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 在新终端中执行自定义命令
|
|
72
|
+
app.post('/api/exec-in-terminal', async (req, res) => {
|
|
73
|
+
try {
|
|
74
|
+
const { command, workingDirectory } = req.body || {};
|
|
75
|
+
if (!command || typeof command !== 'string' || !command.trim()) {
|
|
76
|
+
return res.status(400).json({ success: false, error: 'command 不能为空' });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const targetDir = workingDirectory || getCurrentProjectPath();
|
|
80
|
+
console.log(`在终端中执行命令: ${command}`);
|
|
81
|
+
console.log(`工作目录: ${targetDir}`);
|
|
82
|
+
|
|
83
|
+
const terminalSessionId = nextTerminalSessionId();
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
terminalSessions.set(terminalSessionId, {
|
|
86
|
+
id: terminalSessionId,
|
|
87
|
+
command: command.trim(),
|
|
88
|
+
workingDirectory: targetDir,
|
|
89
|
+
pid: null,
|
|
90
|
+
createdAt: now,
|
|
91
|
+
lastStartedAt: now
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const { pid } = await startTerminalProcess({ command, workingDirectory: targetDir });
|
|
95
|
+
const session = terminalSessions.get(terminalSessionId);
|
|
96
|
+
if (session) {
|
|
97
|
+
session.pid = pid;
|
|
98
|
+
session.lastStartedAt = Date.now();
|
|
99
|
+
terminalSessions.set(terminalSessionId, session);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
res.json({
|
|
103
|
+
success: true,
|
|
104
|
+
message: `已在新终端中执行命令`,
|
|
105
|
+
session: terminalSessions.get(terminalSessionId)
|
|
106
|
+
});
|
|
107
|
+
} catch (error) {
|
|
108
|
+
console.error('在终端中执行命令失败:', error);
|
|
109
|
+
res.status(500).json({
|
|
110
|
+
success: false,
|
|
111
|
+
error: `在终端中执行命令失败: ${error.message}`
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
app.get('/api/terminal-sessions', async (req, res) => {
|
|
117
|
+
try {
|
|
118
|
+
const sessions = Array.from(terminalSessions.values()).sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
119
|
+
res.json({ success: true, sessions });
|
|
120
|
+
} catch (error) {
|
|
121
|
+
res.status(500).json({ success: false, error: error.message });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
function killTerminalPid(pid) {
|
|
126
|
+
if (!pid) return;
|
|
127
|
+
|
|
128
|
+
if (process.platform === 'win32') {
|
|
129
|
+
exec(`taskkill /pid ${pid} /T /F`, (error) => {
|
|
130
|
+
if (error) {
|
|
131
|
+
console.warn(`[终端会话] taskkill 失败(可忽略): ${error?.message || error}`);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
process.kill(pid, 'SIGTERM');
|
|
139
|
+
} catch (e) {
|
|
140
|
+
// ignore
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function isPidAlive(pid) {
|
|
145
|
+
if (!pid) return false;
|
|
146
|
+
|
|
147
|
+
if (process.platform === 'win32') {
|
|
148
|
+
const script = `Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Out-Null; if ($?) { exit 0 } else { exit 1 }`;
|
|
149
|
+
return await new Promise((resolve) => {
|
|
150
|
+
const child = spawn('powershell.exe', ['-NoProfile', '-Command', script], { windowsHide: true });
|
|
151
|
+
child.on('error', () => resolve(false));
|
|
152
|
+
child.on('close', (code) => resolve(code === 0));
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
process.kill(pid, 0);
|
|
158
|
+
return true;
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
app.post('/api/terminal-sessions/:id/restart', async (req, res) => {
|
|
165
|
+
try {
|
|
166
|
+
const id = parseInt(req.params.id, 10);
|
|
167
|
+
if (!Number.isFinite(id)) {
|
|
168
|
+
return res.status(400).json({ success: false, error: 'id 非法' });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const session = terminalSessions.get(id);
|
|
172
|
+
if (!session) {
|
|
173
|
+
return res.status(404).json({ success: false, error: `终端会话 #${id} 不存在` });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const oldPid = session.pid;
|
|
177
|
+
if (oldPid) {
|
|
178
|
+
killTerminalPid(oldPid);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const { pid } = await startTerminalProcess({ command: session.command, workingDirectory: session.workingDirectory });
|
|
182
|
+
session.pid = pid;
|
|
183
|
+
session.lastStartedAt = Date.now();
|
|
184
|
+
terminalSessions.set(id, session);
|
|
185
|
+
|
|
186
|
+
res.json({ success: true, session });
|
|
187
|
+
} catch (error) {
|
|
188
|
+
res.status(500).json({ success: false, error: error.message });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
app.get('/api/terminal-sessions/status', async (req, res) => {
|
|
193
|
+
try {
|
|
194
|
+
const cleanup = String(req.query.cleanup || 'false') === 'true';
|
|
195
|
+
const sessions = Array.from(terminalSessions.values());
|
|
196
|
+
const results = await Promise.all(sessions.map(async (s) => {
|
|
197
|
+
const alive = s?.pid ? await isPidAlive(s.pid) : false;
|
|
198
|
+
return { ...s, alive };
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
if (cleanup) {
|
|
202
|
+
for (const s of results) {
|
|
203
|
+
if (s.pid && !s.alive) {
|
|
204
|
+
terminalSessions.delete(s.id);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const output = cleanup
|
|
210
|
+
? Array.from(terminalSessions.values()).map((s) => {
|
|
211
|
+
const found = results.find(r => r.id === s.id);
|
|
212
|
+
return { ...s, alive: found?.alive ?? false };
|
|
213
|
+
}).sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0))
|
|
214
|
+
: results.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
215
|
+
|
|
216
|
+
res.json({ success: true, sessions: output });
|
|
217
|
+
} catch (error) {
|
|
218
|
+
res.status(500).json({ success: false, error: error.message });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
app.delete('/api/terminal-sessions/:id', async (req, res) => {
|
|
223
|
+
try {
|
|
224
|
+
const id = parseInt(req.params.id, 10);
|
|
225
|
+
if (!Number.isFinite(id)) {
|
|
226
|
+
return res.status(400).json({ success: false, error: 'id 非法' });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const session = terminalSessions.get(id);
|
|
230
|
+
if (!session) {
|
|
231
|
+
return res.status(404).json({ success: false, error: `终端会话 #${id} 不存在` });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (session.pid) {
|
|
235
|
+
killTerminalPid(session.pid);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
terminalSessions.delete(id);
|
|
239
|
+
res.json({ success: true, removedId: id });
|
|
240
|
+
} catch (error) {
|
|
241
|
+
res.status(500).json({ success: false, error: error.message });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
export function registerUiSocketHandlers({
|
|
2
|
+
io,
|
|
3
|
+
getProjectRoomId,
|
|
4
|
+
getCurrentProjectPath,
|
|
5
|
+
getAndBroadcastStatus,
|
|
6
|
+
getCommandHistory,
|
|
7
|
+
clearCommandHistory,
|
|
8
|
+
addCommandToHistory,
|
|
9
|
+
runningProcesses,
|
|
10
|
+
nextProcessId,
|
|
11
|
+
spawn,
|
|
12
|
+
exec,
|
|
13
|
+
path,
|
|
14
|
+
iconv
|
|
15
|
+
}) {
|
|
16
|
+
io.on('connection', (socket) => {
|
|
17
|
+
console.log('客户端已连接:', socket.id);
|
|
18
|
+
|
|
19
|
+
const projectRoomId = getProjectRoomId();
|
|
20
|
+
socket.join(projectRoomId);
|
|
21
|
+
console.log(`客户端 ${socket.id} 已加入房间: ${projectRoomId}`);
|
|
22
|
+
|
|
23
|
+
getAndBroadcastStatus();
|
|
24
|
+
|
|
25
|
+
const history = getCommandHistory();
|
|
26
|
+
socket.emit('initial_command_history', { history });
|
|
27
|
+
|
|
28
|
+
socket.on('request_full_history', () => {
|
|
29
|
+
const fullHistory = getCommandHistory();
|
|
30
|
+
socket.emit('full_command_history', { history: fullHistory });
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
socket.on('clear_command_history', () => {
|
|
34
|
+
const result = clearCommandHistory();
|
|
35
|
+
socket.emit('command_history_cleared', { success: result });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
socket.on('exec_interactive', async (data) => {
|
|
39
|
+
const { command, directory, sessionId } = data;
|
|
40
|
+
|
|
41
|
+
if (!command || typeof command !== 'string' || !command.trim()) {
|
|
42
|
+
socket.emit('interactive_error', {
|
|
43
|
+
sessionId,
|
|
44
|
+
error: 'command 不能为空'
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const currentProjectPath = getCurrentProjectPath();
|
|
50
|
+
const execDirectory = directory && directory.trim()
|
|
51
|
+
? (path.isAbsolute(directory) ? directory : path.join(currentProjectPath, directory))
|
|
52
|
+
: currentProjectPath;
|
|
53
|
+
|
|
54
|
+
console.log(`[交互式命令] ${sessionId}: ${command} (目录: ${execDirectory})`);
|
|
55
|
+
|
|
56
|
+
const processId = nextProcessId();
|
|
57
|
+
const startTime = Date.now();
|
|
58
|
+
|
|
59
|
+
let collectedStdout = '';
|
|
60
|
+
let collectedStderr = '';
|
|
61
|
+
|
|
62
|
+
const childProcess = spawn(command.trim(), [], {
|
|
63
|
+
cwd: execDirectory,
|
|
64
|
+
shell: true,
|
|
65
|
+
env: {
|
|
66
|
+
...process.env,
|
|
67
|
+
GIT_CONFIG_PARAMETERS: "'color.ui=always' 'color.status=always' 'core.quotepath=false'",
|
|
68
|
+
FORCE_COLOR: '3',
|
|
69
|
+
NPM_CONFIG_COLOR: 'always',
|
|
70
|
+
TERM: 'xterm-256color',
|
|
71
|
+
COLORTERM: 'truecolor',
|
|
72
|
+
CLICOLOR_FORCE: '1',
|
|
73
|
+
PYTHONUNBUFFERED: '1'
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
runningProcesses.set(processId, {
|
|
78
|
+
childProcess,
|
|
79
|
+
command: command.trim(),
|
|
80
|
+
startTime,
|
|
81
|
+
directory: execDirectory,
|
|
82
|
+
sessionId
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
console.log(`[交互式命令] 创建进程 #${processId}: ${command.substring(0, 50)}`);
|
|
86
|
+
|
|
87
|
+
socket.emit('interactive_process_id', { sessionId, processId });
|
|
88
|
+
|
|
89
|
+
const isWindows = process.platform === 'win32';
|
|
90
|
+
const cmdBuiltins = ['dir', 'type', 'echo', 'set', 'path', 'cd', 'md', 'rd', 'del', 'copy', 'move', 'ren'];
|
|
91
|
+
const needsGbkConversion = isWindows && cmdBuiltins.some(builtin =>
|
|
92
|
+
command.trim().toLowerCase().startsWith(builtin + ' ') ||
|
|
93
|
+
command.trim().toLowerCase() === builtin
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
childProcess.stdout?.on('data', (stdoutData) => {
|
|
97
|
+
const output = needsGbkConversion ? iconv.decode(stdoutData, 'gbk') : stdoutData.toString('utf8');
|
|
98
|
+
collectedStdout += output;
|
|
99
|
+
socket.emit('interactive_stdout', { sessionId, data: output });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
childProcess.stderr?.on('data', (stderrData) => {
|
|
103
|
+
let output;
|
|
104
|
+
|
|
105
|
+
if (isWindows) {
|
|
106
|
+
const utf8Output = stderrData.toString('utf8');
|
|
107
|
+
|
|
108
|
+
if (!utf8Output.includes('�') || utf8Output.match(/[\u4e00-\u9fa5]/)) {
|
|
109
|
+
output = utf8Output;
|
|
110
|
+
} else {
|
|
111
|
+
try {
|
|
112
|
+
output = iconv.decode(stderrData, 'gbk');
|
|
113
|
+
} catch (e) {
|
|
114
|
+
output = utf8Output;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
output = stderrData.toString('utf8');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
collectedStderr += output;
|
|
122
|
+
socket.emit('interactive_stderr', { sessionId, data: output });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
childProcess.on('close', (code, signal) => {
|
|
126
|
+
runningProcesses.delete(processId);
|
|
127
|
+
console.log(`[交互式命令] 进程 #${processId} 已结束`);
|
|
128
|
+
|
|
129
|
+
const executionTime = Date.now() - startTime;
|
|
130
|
+
const error = code !== 0 ? `Command exited with code ${code}` : null;
|
|
131
|
+
addCommandToHistory(
|
|
132
|
+
command.trim(),
|
|
133
|
+
collectedStdout,
|
|
134
|
+
collectedStderr,
|
|
135
|
+
error,
|
|
136
|
+
executionTime
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
socket.emit('interactive_exit', {
|
|
140
|
+
sessionId,
|
|
141
|
+
code,
|
|
142
|
+
success: code === 0
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
childProcess.on('error', (error) => {
|
|
147
|
+
runningProcesses.delete(processId);
|
|
148
|
+
console.error(`[交互式命令] 进程 #${processId} 出错:`, error);
|
|
149
|
+
|
|
150
|
+
const executionTime = Date.now() - startTime;
|
|
151
|
+
addCommandToHistory(
|
|
152
|
+
command.trim(),
|
|
153
|
+
collectedStdout,
|
|
154
|
+
collectedStderr,
|
|
155
|
+
error.message,
|
|
156
|
+
executionTime
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
socket.emit('interactive_error', {
|
|
160
|
+
sessionId,
|
|
161
|
+
error: error.message
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
socket.on(`interactive_stdin_${sessionId}`, (inputData) => {
|
|
166
|
+
const { input } = inputData;
|
|
167
|
+
console.log(`[交互式命令] 收到 stdin 输入 (${sessionId}):`, input);
|
|
168
|
+
|
|
169
|
+
if (childProcess.stdin && !childProcess.stdin.destroyed) {
|
|
170
|
+
try {
|
|
171
|
+
childProcess.stdin.write(input + '\n');
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error(`[交互式命令] 写入 stdin 失败:`, err);
|
|
174
|
+
socket.emit('interactive_error', {
|
|
175
|
+
sessionId,
|
|
176
|
+
error: `写入输入失败: ${err.message}`
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
socket.on(`interactive_stop_${sessionId}`, () => {
|
|
183
|
+
console.log(`[交互式命令] 收到停止请求 (${sessionId})`);
|
|
184
|
+
|
|
185
|
+
if (childProcess && !childProcess.killed) {
|
|
186
|
+
try {
|
|
187
|
+
if (process.platform === 'win32') {
|
|
188
|
+
exec(`taskkill /pid ${childProcess.pid} /T /F`, (error) => {
|
|
189
|
+
if (error) {
|
|
190
|
+
console.error(`[交互式命令] taskkill 失败:`, error);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
} else {
|
|
194
|
+
childProcess.kill('SIGTERM');
|
|
195
|
+
setTimeout(() => {
|
|
196
|
+
if (!childProcess.killed) {
|
|
197
|
+
childProcess.kill('SIGKILL');
|
|
198
|
+
}
|
|
199
|
+
}, 2000);
|
|
200
|
+
}
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.error(`[交互式命令] 停止进程失败:`, err);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
socket.on('disconnect', () => {
|
|
209
|
+
console.log(`客户端已断开连接: ${socket.id} (房间: ${getProjectRoomId()})`);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function createSavePortToFile({ savePort, fs, path, cwdFn = process.cwd }) {
|
|
2
|
+
let savedPort = null;
|
|
3
|
+
|
|
4
|
+
return async function savePortToFile(port) {
|
|
5
|
+
try {
|
|
6
|
+
if (savePort && savedPort !== port) {
|
|
7
|
+
savedPort = port;
|
|
8
|
+
|
|
9
|
+
const portFilePath = path.join(cwdFn(), '.port');
|
|
10
|
+
await fs.writeFile(portFilePath, port.toString(), 'utf8');
|
|
11
|
+
console.log(`端口号 ${port} 已保存到 ${portFilePath}`);
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const clientPath = path.join(cwdFn(), 'src', 'ui', 'client');
|
|
15
|
+
const envPath = path.join(clientPath, '.env.local');
|
|
16
|
+
|
|
17
|
+
await fs.access(clientPath).catch(() => {
|
|
18
|
+
console.log(`客户端目录 ${clientPath} 不存在,跳过环境变量设置`);
|
|
19
|
+
return Promise.reject(new Error('Client directory not found'));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
await fs.writeFile(envPath, `VITE_BACKEND_PORT=${port}\n`, 'utf8');
|
|
23
|
+
console.log(`端口号环境变量已保存到 ${envPath}`);
|
|
24
|
+
} catch (envError) {
|
|
25
|
+
console.error('保存端口号到环境变量失败,但不影响主要功能:', envError);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('保存端口号到文件失败:', error);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export async function startServerOnAvailablePort({
|
|
2
|
+
httpServer,
|
|
3
|
+
startPort,
|
|
4
|
+
chalk,
|
|
5
|
+
open,
|
|
6
|
+
noOpen,
|
|
7
|
+
isGitRepo,
|
|
8
|
+
savePortToFile,
|
|
9
|
+
maxTries = 100,
|
|
10
|
+
callbackExecutedRef
|
|
11
|
+
}) {
|
|
12
|
+
let currentPort = startPort;
|
|
13
|
+
const maxPort = startPort + maxTries;
|
|
14
|
+
const getCallbackExecuted = () => {
|
|
15
|
+
if (callbackExecutedRef && typeof callbackExecutedRef === 'object' && 'value' in callbackExecutedRef) {
|
|
16
|
+
return Boolean(callbackExecutedRef.value);
|
|
17
|
+
}
|
|
18
|
+
return false;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const setCallbackExecuted = (value) => {
|
|
22
|
+
if (callbackExecutedRef && typeof callbackExecutedRef === 'object' && 'value' in callbackExecutedRef) {
|
|
23
|
+
callbackExecutedRef.value = Boolean(value);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
while (currentPort < maxPort) {
|
|
28
|
+
try {
|
|
29
|
+
if (currentPort > startPort) {
|
|
30
|
+
await new Promise(resolve => setTimeout(resolve, 800));
|
|
31
|
+
console.log(`尝试端口 ${currentPort}...`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await new Promise((resolve, reject) => {
|
|
35
|
+
const errorHandler = (err) => {
|
|
36
|
+
httpServer.removeListener('error', errorHandler);
|
|
37
|
+
reject(err);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
httpServer.once('error', errorHandler);
|
|
41
|
+
|
|
42
|
+
httpServer.listen(currentPort, () => {
|
|
43
|
+
if (getCallbackExecuted()) return;
|
|
44
|
+
setCallbackExecuted(true);
|
|
45
|
+
|
|
46
|
+
httpServer.removeListener('error', errorHandler);
|
|
47
|
+
|
|
48
|
+
console.log(chalk.green('======================================'));
|
|
49
|
+
console.log(chalk.green(` Zen GitSync 服务器已启动`));
|
|
50
|
+
console.log(chalk.green(` 访问地址: http://localhost:${currentPort}`));
|
|
51
|
+
console.log(chalk.green(` 启动时间: ${new Date().toLocaleString()}`));
|
|
52
|
+
|
|
53
|
+
if (isGitRepo) {
|
|
54
|
+
console.log(chalk.green(` 当前目录是Git仓库`));
|
|
55
|
+
} else {
|
|
56
|
+
console.log(chalk.yellow(` 当前目录不是Git仓库,文件监控未启动`));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(chalk.green('======================================'));
|
|
60
|
+
|
|
61
|
+
savePortToFile(currentPort);
|
|
62
|
+
|
|
63
|
+
if (!noOpen) {
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
open(`http://localhost:${currentPort}`);
|
|
66
|
+
}, 0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
resolve();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return currentPort;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
if (err.code === 'EADDRINUSE') {
|
|
76
|
+
console.log(`端口 ${currentPort} 被占用,尝试下一个端口...`);
|
|
77
|
+
currentPort++;
|
|
78
|
+
} else {
|
|
79
|
+
console.error('启动服务器失败:', err);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.error(`无法找到可用端口 (尝试范围: ${startPort}-${maxPort - 1})`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|