zen-gitsync 2.9.5 → 2.9.6
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/package.json +1 -1
- package/src/ui/public/assets/index-BBxepbAQ.js +79 -0
- package/src/ui/public/assets/index-NF8pDlXw.css +1 -0
- package/src/ui/public/assets/{vendor-Ba8bfrbK.js → vendor-PvZNfVU0.js} +1 -1
- package/src/ui/public/index.html +3 -3
- package/src/ui/server/index.js +217 -25
- package/src/ui/public/assets/index-BxBxY90Z.js +0 -79
- package/src/ui/public/assets/index-CQconlCN.css +0 -1
package/src/ui/public/index.html
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
8
|
<title>Zen GitSync</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/vendor-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-BBxepbAQ.js"></script>
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/vendor-PvZNfVU0.js">
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/vendor-BUGaay5Z.css">
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-NF8pDlXw.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="app"></div>
|
package/src/ui/server/index.js
CHANGED
|
@@ -20,6 +20,9 @@ const configManager = config; // 确保 configManager 可用
|
|
|
20
20
|
const runningProcesses = new Map(); // key: processId, value: { childProcess, command, startTime }
|
|
21
21
|
let processIdCounter = 0;
|
|
22
22
|
|
|
23
|
+
const terminalSessions = new Map(); // key: terminalSessionId, value: { id, command, workingDirectory, pid, createdAt, lastStartedAt }
|
|
24
|
+
let terminalSessionIdCounter = 0;
|
|
25
|
+
|
|
23
26
|
// 分支状态缓存
|
|
24
27
|
let branchStatusCache = {
|
|
25
28
|
currentBranch: null,
|
|
@@ -365,6 +368,68 @@ async function startUIServer(noOpen = false, savePort = false) {
|
|
|
365
368
|
});
|
|
366
369
|
}
|
|
367
370
|
});
|
|
371
|
+
|
|
372
|
+
async function startTerminalProcess({ command, workingDirectory }) {
|
|
373
|
+
const targetDir = workingDirectory || currentProjectPath;
|
|
374
|
+
|
|
375
|
+
if (process.platform === 'win32') {
|
|
376
|
+
const cmdToRun = command.trim();
|
|
377
|
+
const safeWorkingDir = String(targetDir).replace(/"/g, '""');
|
|
378
|
+
const safeCmd = String(cmdToRun).replace(/"/g, '""');
|
|
379
|
+
|
|
380
|
+
const psScript = `$p = Start-Process -FilePath "cmd.exe" -ArgumentList "/K", "${safeCmd}" -WorkingDirectory "${safeWorkingDir}" -PassThru; Write-Output $p.Id`;
|
|
381
|
+
|
|
382
|
+
return await new Promise((resolve, reject) => {
|
|
383
|
+
const child = spawn('powershell.exe', ['-NoProfile', '-Command', psScript], {
|
|
384
|
+
windowsHide: true
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
let out = '';
|
|
388
|
+
let err = '';
|
|
389
|
+
|
|
390
|
+
child.stdout?.on('data', (d) => {
|
|
391
|
+
out += d.toString('utf8');
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
child.stderr?.on('data', (d) => {
|
|
395
|
+
err += d.toString('utf8');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
child.on('error', (e) => reject(e));
|
|
399
|
+
|
|
400
|
+
child.on('close', (code) => {
|
|
401
|
+
if (code !== 0) {
|
|
402
|
+
return reject(new Error(err || `Start-Process 失败,退出码: ${code}`));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const pid = parseInt(String(out).trim(), 10);
|
|
406
|
+
if (!Number.isFinite(pid)) {
|
|
407
|
+
return reject(new Error(`无法获取终端 PID: ${out || err || 'unknown'}`));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
resolve({ pid });
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (process.platform === 'darwin') {
|
|
416
|
+
const script = `tell application "Terminal" to do script "cd ${targetDir} && ${command.trim()}"`;
|
|
417
|
+
exec(`osascript -e '${script}'`, (error) => {
|
|
418
|
+
if (error) {
|
|
419
|
+
console.error('打开终端失败:', error);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
return { pid: null };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const terminalCommand = `gnome-terminal -- bash -c "cd ${targetDir} && ${command.trim()}; exec bash" || xterm -e "cd ${targetDir} && ${command.trim()}; bash"`;
|
|
426
|
+
exec(terminalCommand, (error) => {
|
|
427
|
+
if (error) {
|
|
428
|
+
console.error('打开终端失败:', error);
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
return { pid: null };
|
|
432
|
+
}
|
|
368
433
|
|
|
369
434
|
// 在新终端中执行自定义命令
|
|
370
435
|
app.post('/api/exec-in-terminal', async (req, res) => {
|
|
@@ -378,35 +443,30 @@ async function startUIServer(noOpen = false, savePort = false) {
|
|
|
378
443
|
const targetDir = workingDirectory || currentProjectPath;
|
|
379
444
|
console.log(`在终端中执行命令: ${command}`);
|
|
380
445
|
console.log(`工作目录: ${targetDir}`);
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const script = `tell application "Terminal" to do script "cd ${targetDir} && ${command}"`;
|
|
392
|
-
terminalCommand = `osascript -e '${script}'`;
|
|
393
|
-
} else {
|
|
394
|
-
// Linux: 尝试常见的终端模拟器
|
|
395
|
-
terminalCommand = `gnome-terminal -- bash -c "cd ${targetDir} && ${command}; exec bash" || xterm -e "cd ${targetDir} && ${command}; bash"`;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// 执行命令打开新终端(使用已导入的 exec)
|
|
399
|
-
exec(terminalCommand, (error, stdout, stderr) => {
|
|
400
|
-
if (error) {
|
|
401
|
-
console.error('打开终端失败:', error);
|
|
402
|
-
}
|
|
446
|
+
|
|
447
|
+
const terminalSessionId = ++terminalSessionIdCounter;
|
|
448
|
+
const now = Date.now();
|
|
449
|
+
terminalSessions.set(terminalSessionId, {
|
|
450
|
+
id: terminalSessionId,
|
|
451
|
+
command: command.trim(),
|
|
452
|
+
workingDirectory: targetDir,
|
|
453
|
+
pid: null,
|
|
454
|
+
createdAt: now,
|
|
455
|
+
lastStartedAt: now
|
|
403
456
|
});
|
|
404
|
-
|
|
457
|
+
|
|
458
|
+
const { pid } = await startTerminalProcess({ command, workingDirectory: targetDir });
|
|
459
|
+
const session = terminalSessions.get(terminalSessionId);
|
|
460
|
+
if (session) {
|
|
461
|
+
session.pid = pid;
|
|
462
|
+
session.lastStartedAt = Date.now();
|
|
463
|
+
terminalSessions.set(terminalSessionId, session);
|
|
464
|
+
}
|
|
465
|
+
|
|
405
466
|
res.json({
|
|
406
467
|
success: true,
|
|
407
468
|
message: `已在新终端中执行命令`,
|
|
408
|
-
|
|
409
|
-
path: currentProjectPath
|
|
469
|
+
session: terminalSessions.get(terminalSessionId)
|
|
410
470
|
});
|
|
411
471
|
} catch (error) {
|
|
412
472
|
console.error('在终端中执行命令失败:', error);
|
|
@@ -417,6 +477,138 @@ async function startUIServer(noOpen = false, savePort = false) {
|
|
|
417
477
|
}
|
|
418
478
|
});
|
|
419
479
|
|
|
480
|
+
app.get('/api/terminal-sessions', async (req, res) => {
|
|
481
|
+
try {
|
|
482
|
+
const sessions = Array.from(terminalSessions.values()).sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
483
|
+
res.json({ success: true, sessions });
|
|
484
|
+
} catch (error) {
|
|
485
|
+
res.status(500).json({ success: false, error: error.message });
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
function killTerminalPid(pid) {
|
|
490
|
+
if (!pid) return;
|
|
491
|
+
|
|
492
|
+
if (process.platform === 'win32') {
|
|
493
|
+
exec(`taskkill /pid ${pid} /T /F`, (error) => {
|
|
494
|
+
if (error) {
|
|
495
|
+
// 进程可能已退出,Windows 常见提示:ERROR: The process "xxxx" not found.
|
|
496
|
+
// 这里降级为警告,不影响后续流程
|
|
497
|
+
console.warn(`[终端会话] taskkill 失败(可忽略): ${error?.message || error}`);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
process.kill(pid, 'SIGTERM');
|
|
505
|
+
} catch (e) {
|
|
506
|
+
// ignore
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function isPidAlive(pid) {
|
|
511
|
+
if (!pid) return false;
|
|
512
|
+
|
|
513
|
+
if (process.platform === 'win32') {
|
|
514
|
+
const script = `Get-Process -Id ${pid} -ErrorAction SilentlyContinue | Out-Null; if ($?) { exit 0 } else { exit 1 }`;
|
|
515
|
+
return await new Promise((resolve) => {
|
|
516
|
+
const child = spawn('powershell.exe', ['-NoProfile', '-Command', script], { windowsHide: true });
|
|
517
|
+
child.on('error', () => resolve(false));
|
|
518
|
+
child.on('close', (code) => resolve(code === 0));
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
process.kill(pid, 0);
|
|
524
|
+
return true;
|
|
525
|
+
} catch {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
app.post('/api/terminal-sessions/:id/restart', async (req, res) => {
|
|
531
|
+
try {
|
|
532
|
+
const id = parseInt(req.params.id, 10);
|
|
533
|
+
if (!Number.isFinite(id)) {
|
|
534
|
+
return res.status(400).json({ success: false, error: 'id 非法' });
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const session = terminalSessions.get(id);
|
|
538
|
+
if (!session) {
|
|
539
|
+
return res.status(404).json({ success: false, error: `终端会话 #${id} 不存在` });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// 先尝试结束旧窗口(如果仍在运行)
|
|
543
|
+
const oldPid = session.pid;
|
|
544
|
+
if (oldPid) {
|
|
545
|
+
killTerminalPid(oldPid);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const { pid } = await startTerminalProcess({ command: session.command, workingDirectory: session.workingDirectory });
|
|
549
|
+
session.pid = pid;
|
|
550
|
+
session.lastStartedAt = Date.now();
|
|
551
|
+
terminalSessions.set(id, session);
|
|
552
|
+
|
|
553
|
+
res.json({ success: true, session });
|
|
554
|
+
} catch (error) {
|
|
555
|
+
res.status(500).json({ success: false, error: error.message });
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
app.get('/api/terminal-sessions/status', async (req, res) => {
|
|
560
|
+
try {
|
|
561
|
+
const cleanup = String(req.query.cleanup || 'false') === 'true';
|
|
562
|
+
const sessions = Array.from(terminalSessions.values());
|
|
563
|
+
const results = await Promise.all(sessions.map(async (s) => {
|
|
564
|
+
const alive = s?.pid ? await isPidAlive(s.pid) : false;
|
|
565
|
+
return { ...s, alive };
|
|
566
|
+
}));
|
|
567
|
+
|
|
568
|
+
if (cleanup) {
|
|
569
|
+
for (const s of results) {
|
|
570
|
+
if (s.pid && !s.alive) {
|
|
571
|
+
terminalSessions.delete(s.id);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const output = cleanup
|
|
577
|
+
? Array.from(terminalSessions.values()).map((s) => {
|
|
578
|
+
const found = results.find(r => r.id === s.id);
|
|
579
|
+
return { ...s, alive: found?.alive ?? false };
|
|
580
|
+
}).sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0))
|
|
581
|
+
: results.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0));
|
|
582
|
+
|
|
583
|
+
res.json({ success: true, sessions: output });
|
|
584
|
+
} catch (error) {
|
|
585
|
+
res.status(500).json({ success: false, error: error.message });
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
app.delete('/api/terminal-sessions/:id', async (req, res) => {
|
|
590
|
+
try {
|
|
591
|
+
const id = parseInt(req.params.id, 10);
|
|
592
|
+
if (!Number.isFinite(id)) {
|
|
593
|
+
return res.status(400).json({ success: false, error: 'id 非法' });
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const session = terminalSessions.get(id);
|
|
597
|
+
if (!session) {
|
|
598
|
+
return res.status(404).json({ success: false, error: `终端会话 #${id} 不存在` });
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (session.pid) {
|
|
602
|
+
killTerminalPid(session.pid);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
terminalSessions.delete(id);
|
|
606
|
+
res.json({ success: true, removedId: id });
|
|
607
|
+
} catch (error) {
|
|
608
|
+
res.status(500).json({ success: false, error: error.message });
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
|
|
420
612
|
// 停止正在运行的进程
|
|
421
613
|
app.post('/api/kill-process', async (req, res) => {
|
|
422
614
|
try {
|