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,247 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import iconv from 'iconv-lite';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
|
|
5
|
+
export function registerExecRoutes({
|
|
6
|
+
app,
|
|
7
|
+
execGitCommand,
|
|
8
|
+
addCommandToHistory,
|
|
9
|
+
getCurrentProjectPath,
|
|
10
|
+
nextProcessId,
|
|
11
|
+
runningProcesses
|
|
12
|
+
}) {
|
|
13
|
+
// 通用命令执行接口(非流式)
|
|
14
|
+
app.post('/api/exec', async (req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
const { command } = req.body || {};
|
|
17
|
+
if (!command || typeof command !== 'string' || !command.trim()) {
|
|
18
|
+
return res.status(400).json({ success: false, error: 'command 不能为空' });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const { stdout = '', stderr = '' } = await execGitCommand(command, { log: false });
|
|
23
|
+
return res.json({ success: true, stdout, stderr });
|
|
24
|
+
} catch (err) {
|
|
25
|
+
return res.status(400).json({ success: false, error: err?.message || String(err) });
|
|
26
|
+
}
|
|
27
|
+
} catch (error) {
|
|
28
|
+
res.status(500).json({ success: false, error: error.message });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// 流式执行命令接口(支持实时输出)
|
|
33
|
+
app.post('/api/exec-stream', async (req, res) => {
|
|
34
|
+
try {
|
|
35
|
+
const { command, directory } = req.body || {};
|
|
36
|
+
if (!command || typeof command !== 'string' || !command.trim()) {
|
|
37
|
+
return res.status(400).json({ success: false, error: 'command 不能为空' });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const currentProjectPath = getCurrentProjectPath();
|
|
41
|
+
|
|
42
|
+
// 确定执行目录
|
|
43
|
+
const execDirectory = directory && directory.trim()
|
|
44
|
+
? (path.isAbsolute(directory) ? directory : path.join(currentProjectPath, directory))
|
|
45
|
+
: currentProjectPath;
|
|
46
|
+
|
|
47
|
+
console.log(`流式执行命令: ${command}`);
|
|
48
|
+
console.log(`执行目录: ${execDirectory}`);
|
|
49
|
+
|
|
50
|
+
// 分配进程 ID
|
|
51
|
+
const processId = nextProcessId();
|
|
52
|
+
|
|
53
|
+
// 设置响应头为流式传输
|
|
54
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
55
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
56
|
+
res.setHeader('Connection', 'keep-alive');
|
|
57
|
+
res.setHeader('X-Accel-Buffering', 'no'); // 禁用nginx缓冲
|
|
58
|
+
|
|
59
|
+
// 记录执行开始时间(用于命令历史)
|
|
60
|
+
const startTime = Date.now();
|
|
61
|
+
|
|
62
|
+
// 用于收集输出(用于命令历史)
|
|
63
|
+
let collectedStdout = '';
|
|
64
|
+
let collectedStderr = '';
|
|
65
|
+
|
|
66
|
+
// 使用 shell: true 来支持 Windows 内置命令(如 dir、cd 等)
|
|
67
|
+
const childProcess = spawn(command.trim(), [], {
|
|
68
|
+
cwd: execDirectory,
|
|
69
|
+
shell: true, // 通过 shell 执行,支持 Windows 内置命令
|
|
70
|
+
env: {
|
|
71
|
+
...process.env,
|
|
72
|
+
// Git 配置:启用颜色输出和禁用路径引用
|
|
73
|
+
GIT_CONFIG_PARAMETERS: "'color.ui=always' 'color.status=always' 'core.quotepath=false'",
|
|
74
|
+
// 强制启用颜色输出 - 多种工具的配置
|
|
75
|
+
FORCE_COLOR: '3', // 使用级别3(最强),支持 chalk 等库
|
|
76
|
+
NPM_CONFIG_COLOR: 'always',
|
|
77
|
+
TERM: 'xterm-256color', // 模拟256色终端环境
|
|
78
|
+
COLORTERM: 'truecolor', // 支持真彩色
|
|
79
|
+
CLICOLOR_FORCE: '1', // 强制启用颜色(某些工具检测此变量)
|
|
80
|
+
// 确保输出不被缓冲
|
|
81
|
+
PYTHONUNBUFFERED: '1'
|
|
82
|
+
// 注意:不设置 CI=true 和 NO_COLOR,避免禁用颜色输出
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// 存储进程信息
|
|
87
|
+
runningProcesses.set(processId, {
|
|
88
|
+
childProcess,
|
|
89
|
+
command: command.trim(),
|
|
90
|
+
startTime,
|
|
91
|
+
directory: execDirectory
|
|
92
|
+
});
|
|
93
|
+
console.log(`[进程管理] 创建进程 #${processId}: ${command.substring(0, 50)}`);
|
|
94
|
+
|
|
95
|
+
// 发送数据到客户端的辅助函数
|
|
96
|
+
const sendData = (type, data) => {
|
|
97
|
+
const message = `data: ${JSON.stringify({ type, data })}\n\n`;
|
|
98
|
+
// console.log(`[流式输出] 发送数据 - 类型: ${type}, 长度: ${data?.length || 0}`);
|
|
99
|
+
res.write(message);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// 立即发送 processId 给前端
|
|
103
|
+
sendData('process_id', processId);
|
|
104
|
+
|
|
105
|
+
let outputReceived = false;
|
|
106
|
+
|
|
107
|
+
// 判断是否需要 GBK 转换
|
|
108
|
+
// 只有 Windows CMD 内置命令(如 dir、type 等)才需要 GBK 转换
|
|
109
|
+
// npm、node、git 等现代工具都输出 UTF-8
|
|
110
|
+
const isWindows = process.platform === 'win32';
|
|
111
|
+
const cmdBuiltins = ['dir', 'type', 'echo', 'set', 'path', 'cd', 'md', 'rd', 'del', 'copy', 'move', 'ren'];
|
|
112
|
+
const needsGbkConversion = isWindows && cmdBuiltins.some(builtin =>
|
|
113
|
+
command.trim().toLowerCase().startsWith(builtin + ' ') ||
|
|
114
|
+
command.trim().toLowerCase() === builtin
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
console.log(`[流式输出] 命令: ${command.substring(0, 50)}, 需要GBK转换: ${needsGbkConversion}`);
|
|
118
|
+
|
|
119
|
+
// 监听标准输出
|
|
120
|
+
childProcess.stdout?.on('data', (data) => {
|
|
121
|
+
// data 是 Buffer 对象
|
|
122
|
+
let output;
|
|
123
|
+
if (needsGbkConversion) {
|
|
124
|
+
// Windows CMD 内置命令,从 GBK 转换为 UTF-8
|
|
125
|
+
output = iconv.decode(data, 'gbk');
|
|
126
|
+
console.log(`[流式输出] 收到stdout(GBK转UTF8):`, output.substring(0, 200));
|
|
127
|
+
} else {
|
|
128
|
+
// 现代工具或 Unix 系统,直接使用 UTF-8
|
|
129
|
+
output = data.toString('utf8');
|
|
130
|
+
console.log(`[流式输出] 收到stdout(UTF8):`, output.substring(0, 200));
|
|
131
|
+
}
|
|
132
|
+
outputReceived = true;
|
|
133
|
+
collectedStdout += output; // 收集输出用于历史记录
|
|
134
|
+
sendData('stdout', output);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// 监听标准错误输出
|
|
138
|
+
childProcess.stderr?.on('data', (data) => {
|
|
139
|
+
// data 是 Buffer 对象
|
|
140
|
+
let output;
|
|
141
|
+
|
|
142
|
+
if (isWindows) {
|
|
143
|
+
// Windows 平台需要智能检测编码
|
|
144
|
+
// 先尝试 UTF-8 解码
|
|
145
|
+
const utf8Output = data.toString('utf8');
|
|
146
|
+
|
|
147
|
+
// 检测是否包含 UTF-8 替换字符(�),这通常表示解码失败
|
|
148
|
+
// 如果没有替换字符且包含正常字符,说明是有效的 UTF-8
|
|
149
|
+
if (!utf8Output.includes('�') || utf8Output.match(/[\u4e00-\u9fa5]/)) {
|
|
150
|
+
// UTF-8 解码成功(包含有效中文或没有替换字符)
|
|
151
|
+
output = utf8Output;
|
|
152
|
+
console.log(`[流式输出] 收到stderr(UTF8):`, output.substring(0, 200));
|
|
153
|
+
} else {
|
|
154
|
+
// UTF-8 解码失败,尝试 GBK(可能是 CMD shell 的系统消息)
|
|
155
|
+
try {
|
|
156
|
+
output = iconv.decode(data, 'gbk');
|
|
157
|
+
console.log(`[流式输出] 收到stderr(GBK转UTF8):`, output.substring(0, 200));
|
|
158
|
+
} catch (e) {
|
|
159
|
+
// GBK 也失败,使用原始 UTF-8 结果
|
|
160
|
+
output = utf8Output;
|
|
161
|
+
console.log(`[流式输出] GBK解码失败,使用UTF8:`, output.substring(0, 200));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
// Unix系统,直接使用 UTF-8
|
|
166
|
+
output = data.toString('utf8');
|
|
167
|
+
console.log(`[流式输出] 收到stderr(UTF8):`, output.substring(0, 200));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
outputReceived = true;
|
|
171
|
+
collectedStderr += output; // 收集错误输出用于历史记录
|
|
172
|
+
// 不再自动标记为错误,只显示 stderr 输出
|
|
173
|
+
// Git 的警告信息会输出到 stderr 但退出码仍为 0
|
|
174
|
+
sendData('stderr', output);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// 监听进程退出(exit 在流关闭前触发)
|
|
178
|
+
childProcess.on('exit', (code, signal) => {
|
|
179
|
+
// console.log(`[流式输出] 进程 exit 事件 - 代码: ${code}, 信号: ${signal}`);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// 监听进程关闭(close 在流关闭后触发)
|
|
183
|
+
childProcess.on('close', (code, signal) => {
|
|
184
|
+
// console.log(`[流式输出] 进程 close 事件 - 代码: ${code}, 信号: ${signal}, 有输出: ${outputReceived}`);
|
|
185
|
+
|
|
186
|
+
// 从运行进程列表中移除
|
|
187
|
+
runningProcesses.delete(processId);
|
|
188
|
+
console.log(`[进程管理] 进程 #${processId} 已结束,剩余进程数: ${runningProcesses.size}`);
|
|
189
|
+
|
|
190
|
+
// 计算执行时间
|
|
191
|
+
const executionTime = Date.now() - startTime;
|
|
192
|
+
|
|
193
|
+
// 添加到命令历史
|
|
194
|
+
const error = code !== 0 ? `Command exited with code ${code}` : null;
|
|
195
|
+
addCommandToHistory(
|
|
196
|
+
command.trim(),
|
|
197
|
+
collectedStdout,
|
|
198
|
+
collectedStderr,
|
|
199
|
+
error,
|
|
200
|
+
executionTime
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// 只根据退出码判断成功与否,退出码为 0 表示成功
|
|
204
|
+
sendData('exit', { code, success: code === 0 });
|
|
205
|
+
res.end();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// 监听错误
|
|
209
|
+
childProcess.on('error', (error) => {
|
|
210
|
+
// console.error(`[流式输出] 进程错误:`, error);
|
|
211
|
+
|
|
212
|
+
// 从运行进程列表中移除
|
|
213
|
+
runningProcesses.delete(processId);
|
|
214
|
+
console.log(`[进程管理] 进程 #${processId} 出错并结束,剩余进程数: ${runningProcesses.size}`);
|
|
215
|
+
|
|
216
|
+
// 添加到命令历史(错误情况)
|
|
217
|
+
const executionTime = Date.now() - startTime;
|
|
218
|
+
addCommandToHistory(
|
|
219
|
+
command.trim(),
|
|
220
|
+
collectedStdout,
|
|
221
|
+
collectedStderr,
|
|
222
|
+
error.message,
|
|
223
|
+
executionTime
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
sendData('error', error.message);
|
|
227
|
+
res.end();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// 添加spawn事件监听
|
|
231
|
+
childProcess.on('spawn', () => {
|
|
232
|
+
// console.log(`[流式输出] 进程已启动 - PID: ${childProcess.pid}`);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// 注意:不监听req.on('close'),参考git push的实现
|
|
236
|
+
// 进程会自然结束,close事件会触发res.end()
|
|
237
|
+
// 如果监听req.on('close')可能会导致进程被提前kill
|
|
238
|
+
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error('流式执行命令失败:', error);
|
|
241
|
+
res.status(500).json({
|
|
242
|
+
success: false,
|
|
243
|
+
error: `流式执行命令失败: ${error.message}`
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import open from 'open';
|
|
4
|
+
import { spawn } from 'child_process';
|
|
5
|
+
|
|
6
|
+
export function registerFileOpenRoutes({
|
|
7
|
+
app
|
|
8
|
+
}) {
|
|
9
|
+
// 打开文件
|
|
10
|
+
app.post('/api/open-file', async (req, res) => {
|
|
11
|
+
try {
|
|
12
|
+
const { filePath, context } = req.body;
|
|
13
|
+
|
|
14
|
+
if (!filePath) {
|
|
15
|
+
return res.status(400).json({
|
|
16
|
+
success: false,
|
|
17
|
+
error: '文件路径不能为空'
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let targetFilePath = filePath;
|
|
22
|
+
|
|
23
|
+
// 根据上下文处理不同的文件打开方式
|
|
24
|
+
switch (context) {
|
|
25
|
+
case 'git-status':
|
|
26
|
+
// Git状态:直接打开当前工作目录中的文件
|
|
27
|
+
targetFilePath = path.resolve(process.cwd(), filePath);
|
|
28
|
+
break;
|
|
29
|
+
|
|
30
|
+
case 'commit-detail':
|
|
31
|
+
// 提交详情:这里可以考虑创建临时文件显示该提交时的文件内容
|
|
32
|
+
// 暂时先打开当前版本的文件
|
|
33
|
+
targetFilePath = path.resolve(process.cwd(), filePath);
|
|
34
|
+
break;
|
|
35
|
+
|
|
36
|
+
case 'stash-detail':
|
|
37
|
+
// Stash详情:同样暂时打开当前版本的文件
|
|
38
|
+
targetFilePath = path.resolve(process.cwd(), filePath);
|
|
39
|
+
break;
|
|
40
|
+
|
|
41
|
+
default:
|
|
42
|
+
targetFilePath = path.resolve(process.cwd(), filePath);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
// 检查文件是否存在
|
|
47
|
+
await fs.access(targetFilePath);
|
|
48
|
+
|
|
49
|
+
// 使用系统默认程序打开文件
|
|
50
|
+
await open(targetFilePath, { wait: false });
|
|
51
|
+
|
|
52
|
+
res.json({
|
|
53
|
+
success: true,
|
|
54
|
+
message: `已打开文件: ${path.basename(targetFilePath)}`
|
|
55
|
+
});
|
|
56
|
+
} catch (error) {
|
|
57
|
+
// 如果文件不存在,尝试在编辑器中创建新文件
|
|
58
|
+
if (error.code === 'ENOENT') {
|
|
59
|
+
try {
|
|
60
|
+
await open(targetFilePath, { wait: false });
|
|
61
|
+
res.json({
|
|
62
|
+
success: true,
|
|
63
|
+
message: `已在编辑器中打开文件: ${path.basename(targetFilePath)}`
|
|
64
|
+
});
|
|
65
|
+
} catch (openError) {
|
|
66
|
+
res.status(400).json({
|
|
67
|
+
success: false,
|
|
68
|
+
error: `无法打开文件 "${path.basename(targetFilePath)}": ${openError.message}`
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
res.status(400).json({
|
|
73
|
+
success: false,
|
|
74
|
+
error: `无法访问文件 "${path.basename(targetFilePath)}": ${error.message}`
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
res.status(500).json({
|
|
80
|
+
success: false,
|
|
81
|
+
error: error.message
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// 用VSCode打开文件
|
|
87
|
+
app.post('/api/open-with-vscode', async (req, res) => {
|
|
88
|
+
try {
|
|
89
|
+
const { filePath, context } = req.body;
|
|
90
|
+
|
|
91
|
+
if (!filePath) {
|
|
92
|
+
return res.status(400).json({
|
|
93
|
+
success: false,
|
|
94
|
+
error: '文件路径不能为空'
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let targetFilePath = filePath;
|
|
99
|
+
|
|
100
|
+
// 根据上下文处理不同的文件打开方式
|
|
101
|
+
switch (context) {
|
|
102
|
+
case 'git-status':
|
|
103
|
+
case 'commit-detail':
|
|
104
|
+
case 'stash-detail':
|
|
105
|
+
targetFilePath = path.resolve(process.cwd(), filePath);
|
|
106
|
+
break;
|
|
107
|
+
default:
|
|
108
|
+
targetFilePath = path.resolve(process.cwd(), filePath);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
// 使用VSCode打开文件
|
|
113
|
+
// 尝试使用 'code' 命令打开文件
|
|
114
|
+
// 使用已导入的 spawn
|
|
115
|
+
|
|
116
|
+
// 创建一个Promise来处理spawn的异步结果
|
|
117
|
+
const spawnPromise = new Promise((resolve, reject) => {
|
|
118
|
+
const vscodeProcess = spawn('code', [targetFilePath], {
|
|
119
|
+
detached: true,
|
|
120
|
+
stdio: 'ignore'
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// 监听错误事件
|
|
124
|
+
vscodeProcess.on('error', (err) => {
|
|
125
|
+
reject(err);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// 监听spawn事件,表示进程成功启动
|
|
129
|
+
vscodeProcess.on('spawn', () => {
|
|
130
|
+
resolve('success');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
vscodeProcess.unref();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await spawnPromise;
|
|
137
|
+
|
|
138
|
+
res.json({
|
|
139
|
+
success: true,
|
|
140
|
+
message: `已用VSCode打开文件: ${path.basename(targetFilePath)}`
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
// 如果VSCode命令不可用,尝试直接用open打开
|
|
144
|
+
try {
|
|
145
|
+
await open(targetFilePath, { app: { name: 'code' } });
|
|
146
|
+
res.json({
|
|
147
|
+
success: true,
|
|
148
|
+
message: `已用VSCode打开文件: ${path.basename(targetFilePath)}`
|
|
149
|
+
});
|
|
150
|
+
} catch (openError) {
|
|
151
|
+
// 最后的备用方案:尝试用系统默认编辑器打开
|
|
152
|
+
try {
|
|
153
|
+
await open(targetFilePath);
|
|
154
|
+
res.json({
|
|
155
|
+
success: true,
|
|
156
|
+
message: `VSCode不可用,已用系统默认程序打开文件: ${path.basename(targetFilePath)}`
|
|
157
|
+
});
|
|
158
|
+
} catch (finalError) {
|
|
159
|
+
res.status(400).json({
|
|
160
|
+
success: false,
|
|
161
|
+
error: `无法打开文件 "${path.basename(targetFilePath)}": VSCode可能未安装或未添加到PATH,且系统默认程序也无法打开该文件`
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
res.status(500).json({
|
|
168
|
+
success: false,
|
|
169
|
+
error: error.message
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|