claude-opencode-viewer 2.6.29 → 2.6.31

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +52 -30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-opencode-viewer",
3
- "version": "2.6.29",
3
+ "version": "2.6.31",
4
4
  "description": "A unified terminal viewer for Claude Code and OpenCode with seamless switching",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -199,6 +199,20 @@ function killProcessTree(proc) {
199
199
  }, 1000);
200
200
  }
201
201
 
202
+ // 清理孤儿进程(PPID=1 的 opencode/claude)
203
+ function cleanupOrphanProcesses() {
204
+ try {
205
+ const orphans = execSync(
206
+ "ps -eo pid,ppid,comm 2>/dev/null | awk '$2==1 && (/opencode/||/claude/) {print $1}'",
207
+ { encoding: 'utf-8', timeout: 5000 }
208
+ ).trim().split(/\s+/).filter(Boolean).map(Number);
209
+ for (const pid of orphans) {
210
+ try { process.kill(pid, 'SIGKILL'); } catch {}
211
+ }
212
+ if (orphans.length > 0) LOG(`[cleanup] 清理孤儿进程: ${orphans.join(', ')}`);
213
+ } catch {}
214
+ }
215
+
202
216
  async function spawnProcess(mode, sessionId = null) {
203
217
  const pty = await getPty();
204
218
  fixSpawnHelperPermissions();
@@ -336,8 +350,14 @@ async function switchMode(newMode) {
336
350
  }
337
351
  currentProcess = null;
338
352
 
339
- // 等待一小段时间确保进程完全退出
340
- await new Promise(resolve => setTimeout(resolve, 100));
353
+ // 等待旧进程完全退出(Bun crash dump 可能延迟输出)
354
+ await new Promise(resolve => setTimeout(resolve, 500));
355
+
356
+ // 清理可能的孤儿进程(Bun segfault 导致子进程未被回收)
357
+ cleanupOrphanProcesses();
358
+
359
+ // 清空旧进程残留输出(包括 Bun crash dump)
360
+ outputBuffer = '';
341
361
 
342
362
  // 切换到新模式
343
363
  currentMode = newMode;
@@ -673,22 +693,32 @@ const requestHandler = async (req, res) => {
673
693
  return;
674
694
  }
675
695
 
676
- // API: 获取文档列表
696
+ // API: 获取文档列表(扫描 /halo 或 cwd 下的 md 文件,最多两层目录)
677
697
  if (req.url === '/api/docs') {
678
698
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
679
699
  try {
680
- const docsCwd = existsSync('/halo/code') ? '/halo/code' : process.cwd();
700
+ const docsRoot = existsSync('/halo') ? '/halo' : process.cwd();
681
701
  const EXCLUDE = new Set(['readme.md', 'changelog.md', 'license.md', 'claude.md', 'agents.md', 'contributing.md', 'security.md', 'context.md']);
682
- const files = readdirSync(docsCwd)
683
- .filter(f => f.endsWith('.md') && !EXCLUDE.has(f.toLowerCase()))
684
- .map(f => {
685
- try {
686
- const st = statSync(join(docsCwd, f));
687
- return { name: f, mtime: st.mtimeMs };
688
- } catch { return null; }
689
- })
690
- .filter(Boolean)
691
- .sort((a, b) => b.mtime - a.mtime);
702
+ const files = [];
703
+ const SKIP_DIRS = new Set(['node_modules', '.git', '.opencode', '.claude', '.idea', '.vscode']);
704
+ const scanDir = (dir, prefix, depth) => {
705
+ if (depth > 5) return; // 最多 5 层,避免过深
706
+ try {
707
+ for (const f of readdirSync(dir, { withFileTypes: true })) {
708
+ if (f.isFile() && f.name.endsWith('.md') && !EXCLUDE.has(f.name.toLowerCase())) {
709
+ try {
710
+ const fullPath = join(dir, f.name);
711
+ const st = statSync(fullPath);
712
+ files.push({ name: join(dir, f.name), mtime: st.mtimeMs });
713
+ } catch {}
714
+ } else if (f.isDirectory() && !f.name.startsWith('.') && !SKIP_DIRS.has(f.name)) {
715
+ scanDir(join(dir, f.name), prefix ? prefix + '/' + f.name : f.name, depth + 1);
716
+ }
717
+ }
718
+ } catch {}
719
+ };
720
+ scanDir(docsRoot, '', 0);
721
+ files.sort((a, b) => b.mtime - a.mtime);
692
722
  res.end(JSON.stringify({ docs: files }));
693
723
  } catch (err) {
694
724
  res.end(JSON.stringify({ docs: [], error: err.message }));
@@ -701,13 +731,12 @@ const requestHandler = async (req, res) => {
701
731
  const url = new URL(req.url, `http://${req.headers.host}`);
702
732
  const file = url.searchParams.get('file');
703
733
  res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
704
- if (!file || file.includes('/') || file.includes('..') || !file.endsWith('.md')) {
734
+ if (!file || file.includes('..') || !file.endsWith('.md')) {
705
735
  res.end(JSON.stringify({ error: '无效文件名' }));
706
736
  return;
707
737
  }
708
738
  try {
709
- const docsCwd = existsSync('/halo/code') ? '/halo/code' : process.cwd();
710
- const filePath = join(docsCwd, file);
739
+ const filePath = file;
711
740
  if (!existsSync(filePath)) {
712
741
  res.end(JSON.stringify({ error: '文件不存在' }));
713
742
  return;
@@ -933,7 +962,8 @@ wssInst.on('connection', (ws, req) => {
933
962
  outputBuffer = '';
934
963
 
935
964
  // 等待进程完全退出
936
- await new Promise(resolve => setTimeout(resolve, 200));
965
+ await new Promise(resolve => setTimeout(resolve, 500));
966
+ cleanupOrphanProcesses();
937
967
 
938
968
  // 启动新的 opencode 进程,传入 session ID
939
969
  try {
@@ -978,7 +1008,8 @@ wssInst.on('connection', (ws, req) => {
978
1008
  }
979
1009
 
980
1010
  outputBuffer = '';
981
- await new Promise(resolve => setTimeout(resolve, 200));
1011
+ await new Promise(resolve => setTimeout(resolve, 500));
1012
+ cleanupOrphanProcesses();
982
1013
 
983
1014
  // 先通知前端准备好,再启动新进程
984
1015
  ws.send(JSON.stringify({ type: 'new-session-ok', mode }));
@@ -1089,17 +1120,8 @@ function startServer() {
1089
1120
  LOG('\n按 Ctrl+C 停止服务\n');
1090
1121
  }
1091
1122
 
1092
- // 清理上次 cov 崩溃残留的孤儿进程(PPID=1 的 opencode/claude)
1093
- try {
1094
- const orphans = execSync(
1095
- "ps -eo pid,ppid,comm 2>/dev/null | awk '$2==1 && (/opencode/||/claude/) {print $1}'",
1096
- { encoding: 'utf-8', timeout: 5000 }
1097
- ).trim().split(/\s+/).filter(Boolean).map(Number);
1098
- for (const pid of orphans) {
1099
- try { process.kill(pid, 'SIGTERM'); } catch {}
1100
- }
1101
- if (orphans.length > 0) LOG(`[startup] 清理孤儿进程: ${orphans.join(', ')}`);
1102
- } catch {}
1123
+ // 清理上次 cov 崩溃残留的孤儿进程
1124
+ cleanupOrphanProcesses();
1103
1125
 
1104
1126
  // 尝试恢复最近的会话,如果没有则新建
1105
1127
  let lastSessionId = null;