triflux 8.2.1 → 8.2.3

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/bin/triflux.mjs CHANGED
@@ -1382,7 +1382,7 @@ async function cmdDoctor(options = {}) {
1382
1382
  if (process.platform === "win32") {
1383
1383
  try {
1384
1384
  const { cleanupOrphanNodeProcesses } = await import("../hub/lib/process-utils.mjs");
1385
- if (autoFix) {
1385
+ if (fix) {
1386
1386
  const { killed, remaining } = cleanupOrphanNodeProcesses();
1387
1387
  if (killed > 0) {
1388
1388
  warn(`고아 node.exe ${killed}개 정리 완료 (남은 프로세스: ${remaining})`);
@@ -2,10 +2,18 @@
2
2
  // 프로세스 관련 공유 유틸리티
3
3
 
4
4
  import { execSync } from "node:child_process";
5
- import { existsSync, readFileSync } from "node:fs";
6
- import { homedir } from "node:os";
5
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs";
6
+ import { homedir, tmpdir } from "node:os";
7
7
  import { join } from "node:path";
8
8
 
9
+ const CLEANUP_SCRIPT_DIR = join(tmpdir(), "tfx-process-utils");
10
+ const SCAN_SCRIPT_PATH = join(CLEANUP_SCRIPT_DIR, "scan-processes.ps1");
11
+ const TREE_SCRIPT_PATH = join(CLEANUP_SCRIPT_DIR, "get-ancestor-tree.ps1");
12
+
13
+ // 스크립트 버전 — 내용 변경 시 증가하여 캐시된 스크립트를 갱신
14
+ const SCRIPT_VERSION = 2;
15
+ const VERSION_FILE = join(CLEANUP_SCRIPT_DIR, ".version");
16
+
9
17
  /**
10
18
  * 주어진 PID의 프로세스가 살아있는지 확인한다.
11
19
  * EPERM: 프로세스는 존재하지만 signal 권한 없음 → alive
@@ -25,15 +33,109 @@ export function isPidAlive(pid) {
25
33
  }
26
34
 
27
35
  /**
28
- * 부모 프로세스가 죽은 고아 node.exe 프로세스를 정리한다.
29
- * Windows 전용 Agent 서브프로세스가 MCP 서버를 남기는 문제 대응.
36
+ * PowerShell 헬퍼 스크립트를 임시 디렉토리에 생성한다.
37
+ * bash의 $_ 이스케이핑 문제를 피하기 위해 -File로 실행.
38
+ */
39
+ function ensureHelperScripts() {
40
+ mkdirSync(CLEANUP_SCRIPT_DIR, { recursive: true });
41
+
42
+ // 버전 체크 — 스크립트 갱신 필요 여부
43
+ let needsUpdate = true;
44
+ try {
45
+ if (existsSync(VERSION_FILE)) {
46
+ const cached = Number.parseInt(readFileSync(VERSION_FILE, "utf8").trim(), 10);
47
+ if (cached === SCRIPT_VERSION) needsUpdate = false;
48
+ }
49
+ } catch {}
50
+
51
+ if (needsUpdate) {
52
+ // 기존 스크립트 삭제 후 재생성
53
+ try { unlinkSync(SCAN_SCRIPT_PATH); } catch {}
54
+ try { unlinkSync(TREE_SCRIPT_PATH); } catch {}
55
+ }
56
+
57
+ if (!existsSync(TREE_SCRIPT_PATH)) {
58
+ writeFileSync(TREE_SCRIPT_PATH, [
59
+ "param([int]$StartPid)",
60
+ "$p = $StartPid",
61
+ "for ($i = 0; $i -lt 10; $i++) {",
62
+ " if ($p -le 0) { break }",
63
+ " Write-Output $p",
64
+ ' $parent = (Get-CimInstance Win32_Process -Filter "ProcessId=$p" -ErrorAction SilentlyContinue).ParentProcessId',
65
+ " if ($null -eq $parent -or $parent -le 0) { break }",
66
+ " $p = $parent",
67
+ "}",
68
+ ].join("\n"), "utf8");
69
+ }
70
+
71
+ if (!existsSync(SCAN_SCRIPT_PATH)) {
72
+ // node.exe + bash.exe + cmd.exe 전체를 스캔하여 PID,ParentPID,Name 출력
73
+ writeFileSync(SCAN_SCRIPT_PATH, [
74
+ "$ErrorActionPreference = 'SilentlyContinue'",
75
+ "Get-CimInstance Win32_Process -Filter \"Name='node.exe' OR Name='bash.exe' OR Name='cmd.exe'\" | ForEach-Object {",
76
+ ' Write-Output "$($_.ProcessId),$($_.ParentProcessId),$($_.Name)"',
77
+ "}",
78
+ ].join("\n"), "utf8");
79
+ }
80
+
81
+ if (needsUpdate) {
82
+ writeFileSync(VERSION_FILE, String(SCRIPT_VERSION), "utf8");
83
+ }
84
+ }
85
+
86
+ /**
87
+ * PID → 루트 조상까지의 체인에서 살아있는 조상이 있는지 확인한다.
88
+ * 프로세스 맵을 사용하여 O(depth) 탐색.
89
+ * @param {number} pid
90
+ * @param {Map<number, {ppid: number, name: string}>} procMap
91
+ * @param {Set<number>} protectedPids
92
+ * @returns {boolean} true = 보호됨 (활성 조상 체인이 있음)
93
+ */
94
+ function hasLiveAncestorChain(pid, procMap, protectedPids) {
95
+ const visited = new Set();
96
+ let current = pid;
97
+
98
+ while (current > 0 && !visited.has(current)) {
99
+ visited.add(current);
100
+
101
+ if (protectedPids.has(current)) return true;
102
+
103
+ const info = procMap.get(current);
104
+ if (!info) {
105
+ // 프로세스 맵에 없음 → 살아있는지 직접 확인
106
+ return isPidAlive(current);
107
+ }
108
+
109
+ const ppid = info.ppid;
110
+ if (!Number.isFinite(ppid) || ppid <= 0) {
111
+ // 루트 프로세스 (ppid=0) — 시스템 프로세스이므로 보호
112
+ return true;
113
+ }
114
+
115
+ // 부모가 맵에 없고 죽었으면 → 고아 체인
116
+ if (!procMap.has(ppid) && !isPidAlive(ppid)) return false;
117
+
118
+ current = ppid;
119
+ }
120
+
121
+ return false;
122
+ }
123
+
124
+ /**
125
+ * 고아 프로세스 트리를 정리한다 (node.exe + bash.exe + cmd.exe).
126
+ * Windows 전용 — Agent 서브프로세스가 MCP 서버, bash 래퍼, cmd 래퍼를 남기는 문제 대응.
127
+ *
128
+ * 전략: 부모 체인을 루트까지 추적하여, 체인 중간에 죽은 프로세스가 있으면
129
+ * 해당 프로세스 아래의 전체 트리를 고아로 판정하고 정리.
30
130
  *
31
- * 보호 대상: 현재 프로세스, Hub PID, 살아있는 부모를 가진 프로세스
131
+ * 보호 대상: 현재 프로세스 조상 트리, Hub PID
32
132
  * @returns {{ killed: number, remaining: number }}
33
133
  */
34
134
  export function cleanupOrphanNodeProcesses() {
35
135
  if (process.platform !== "win32") return { killed: 0, remaining: 0 };
36
136
 
137
+ ensureHelperScripts();
138
+
37
139
  const myPid = process.pid;
38
140
 
39
141
  // Hub PID 보호
@@ -52,9 +154,8 @@ export function cleanupOrphanNodeProcesses() {
52
154
  if (Number.isFinite(hubPid) && hubPid > 0) protectedPids.add(hubPid);
53
155
 
54
156
  try {
55
- // 현재 프로세스의 조상 트리를 보호 목록에 추가
56
157
  const treeOutput = execSync(
57
- `powershell -NoProfile -Command "$p=${myPid}; for($i=0;$i -lt 10;$i++){if($p -le 0){break}; Write-Output $p; $p=(Get-CimInstance Win32_Process -Filter \\"ProcessId=$p\\").ParentProcessId}"`,
158
+ `powershell -NoProfile -ExecutionPolicy Bypass -File "${TREE_SCRIPT_PATH}" -StartPid ${myPid}`,
58
159
  { encoding: "utf8", timeout: 8000, stdio: ["pipe", "pipe", "pipe"] },
59
160
  );
60
161
  for (const line of treeOutput.split(/\r?\n/)) {
@@ -63,34 +164,40 @@ export function cleanupOrphanNodeProcesses() {
63
164
  }
64
165
  } catch {}
65
166
 
66
- let killed = 0;
167
+ // 전체 프로세스 맵 구축 (node + bash + cmd)
168
+ const procMap = new Map();
67
169
  try {
68
- // 부모가 죽은 고아 node.exe 찾기
69
170
  const output = execSync(
70
- `powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter \\"Name='node.exe'\\" | Select-Object ProcessId,ParentProcessId | ForEach-Object { Write-Output \\"\\"$($_.ProcessId),$($_.ParentProcessId)\\" }"`,
171
+ `powershell -NoProfile -ExecutionPolicy Bypass -File "${SCAN_SCRIPT_PATH}"`,
71
172
  { encoding: "utf8", timeout: 15000, stdio: ["pipe", "pipe", "pipe"] },
72
173
  );
73
174
 
74
175
  for (const line of output.split(/\r?\n/)) {
75
- const trimmed = line.trim().replace(/^"|"$/g, "");
176
+ const trimmed = line.trim();
76
177
  if (!trimmed) continue;
77
- const [pidStr, ppidStr] = trimmed.split(",");
178
+ const [pidStr, ppidStr, name] = trimmed.split(",");
78
179
  const pid = Number.parseInt(pidStr, 10);
79
180
  const ppid = Number.parseInt(ppidStr, 10);
181
+ if (Number.isFinite(pid) && pid > 0) {
182
+ procMap.set(pid, { ppid, name: name || "unknown" });
183
+ }
184
+ }
185
+ } catch {}
80
186
 
81
- if (!Number.isFinite(pid) || pid <= 0) continue;
82
- if (protectedPids.has(pid)) continue;
187
+ // 고아 판정 + 정리
188
+ let killed = 0;
189
+ for (const [pid, info] of procMap) {
190
+ if (protectedPids.has(pid)) continue;
83
191
 
84
- // 부모가 살아있으면 건드리지 않음
85
- if (Number.isFinite(ppid) && ppid > 0 && isPidAlive(ppid)) continue;
192
+ // 조상 체인이 살아있으면 건드리지 않음
193
+ if (hasLiveAncestorChain(pid, procMap, protectedPids)) continue;
86
194
 
87
- // 고아 프로세스 종료
88
- try {
89
- process.kill(pid, "SIGTERM");
90
- killed++;
91
- } catch {}
92
- }
93
- } catch {}
195
+ // 고아 종료
196
+ try {
197
+ process.kill(pid, "SIGTERM");
198
+ killed++;
199
+ } catch {}
200
+ }
94
201
 
95
202
  // 남은 프로세스 수 확인
96
203
  let remaining = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "8.2.1",
3
+ "version": "8.2.3",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {