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.
@@ -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-BxBxY90Z.js"></script>
10
- <link rel="modulepreload" crossorigin href="/assets/vendor-Ba8bfrbK.js">
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-CQconlCN.css">
12
+ <link rel="stylesheet" crossorigin href="/assets/index-NF8pDlXw.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="app"></div>
@@ -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
- let terminalCommand;
384
-
385
- if (process.platform === 'win32') {
386
- // Windows: 使用 start 命令打开新的 cmd 窗口
387
- // /K 参数表示执行命令后保持窗口打开
388
- terminalCommand = `start cmd /K "cd /d ${targetDir} && ${command}"`;
389
- } else if (process.platform === 'darwin') {
390
- // macOS: 使用 osascript 打开 Terminal.app
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
- command: command,
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 {