triflux 9.8.1 → 9.8.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
@@ -28,6 +28,7 @@ import {
28
28
  extractManagedHookFilename, getManagedRegistryHooks, ensureHooksInSettings,
29
29
  ensureCodexHubServerConfig,
30
30
  } from "../scripts/setup.mjs";
31
+ import { cleanupTmpFiles } from "../scripts/tmp-cleanup.mjs";
31
32
 
32
33
  const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
33
34
  const CLAUDE_DIR = join(homedir(), ".claude");
@@ -3531,6 +3532,10 @@ async function main() {
3531
3532
  const cmd = NORMALIZED_ARGS[0] || "help";
3532
3533
  const cmdArgs = NORMALIZED_ARGS.slice(1);
3533
3534
 
3535
+ cleanupTmpFiles({
3536
+ protectPaths: [process.env.HOME, process.env.USERPROFILE],
3537
+ }).catch(() => {});
3538
+
3534
3539
  switch (cmd) {
3535
3540
  case "setup":
3536
3541
  cmdSetup({ dryRun: cmdArgs.includes("--dry-run") });
@@ -12,7 +12,7 @@ import { readFileSync } from "node:fs";
12
12
 
13
13
  // ── 차단 규칙 ──────────────────────────────────────────────
14
14
  const BLOCK_RULES = [
15
- { pattern: /\brm\s+(-[^\s]*)?-rf?\s+[/~](?!\S*node_modules)/i, reason: "루트/홈 디렉토리 rm -rf 차단" },
15
+ { pattern: /\brm\s+(-[^\s]*)?-rf?\s+[/~](?!tmp\b)(?!\S*node_modules)/i, reason: "루트/홈 디렉토리 rm -rf 차단" },
16
16
  { pattern: /\brm\s+(-[^\s]*)?-rf?\s+\.\s*$/i, reason: "현재 디렉토리 rm -rf . 차단" },
17
17
  { pattern: /\bgit\s+push\s+.*--force\s+.*\b(main|master)\b/i, reason: "main/master force push 차단" },
18
18
  { pattern: /\bgit\s+push\s+--force\s*$/i, reason: "대상 미지정 force push 차단" },
@@ -1,15 +1,12 @@
1
1
  // hub/assign-callbacks.mjs — assign job 상태 변경용 Named Pipe/Unix socket 브로드캐스터
2
2
 
3
- import net from 'node:net';
4
- import { existsSync, unlinkSync } from 'node:fs';
5
- import { join } from 'node:path';
6
-
7
- export function getAssignCallbackPipePath(sessionId = process.pid) {
8
- if (process.platform === 'win32') {
9
- return `\\\\.\\pipe\\triflux-assign-callback-${sessionId}`;
10
- }
11
- return join('/tmp', `triflux-assign-callback-${sessionId}.sock`);
12
- }
3
+ import net from 'node:net';
4
+ import { existsSync, unlinkSync } from 'node:fs';
5
+ import { IS_WINDOWS, pipePath } from './platform.mjs';
6
+
7
+ export function getAssignCallbackPipePath(sessionId = process.pid) {
8
+ return pipePath('triflux-assign-callback', sessionId);
9
+ }
13
10
 
14
11
  function buildAssignCallbackEvent(event = {}, row = null) {
15
12
  const source = row || event || {};
@@ -89,9 +86,9 @@ export function createAssignCallbackServer({ store = null, sessionId = process.p
89
86
  },
90
87
  async start() {
91
88
  if (server) return { path: pipePath };
92
- if (process.platform !== 'win32' && existsSync(pipePath)) {
93
- try { unlinkSync(pipePath); } catch {}
94
- }
89
+ if (!IS_WINDOWS && existsSync(pipePath)) {
90
+ try { unlinkSync(pipePath); } catch {}
91
+ }
95
92
 
96
93
  server = net.createServer((socket) => {
97
94
  clients.add(socket);
@@ -127,9 +124,9 @@ export function createAssignCallbackServer({ store = null, sessionId = process.p
127
124
  }
128
125
  await new Promise((resolve) => server.close(resolve));
129
126
  server = null;
130
- if (process.platform !== 'win32' && existsSync(pipePath)) {
131
- try { unlinkSync(pipePath); } catch {}
132
- }
127
+ if (!IS_WINDOWS && existsSync(pipePath)) {
128
+ try { unlinkSync(pipePath); } catch {}
129
+ }
133
130
  },
134
131
  broadcast,
135
132
  };
package/hub/hitl.mjs CHANGED
@@ -109,8 +109,7 @@ export function createHitlManager(store, router = null) {
109
109
  const expired = pending.filter(hr => hr.deadline_ms <= now);
110
110
  if (!expired.length) return 0;
111
111
 
112
- // 트랜잭션으로 만료 요청을 일괄 처리해 DB 왕복을 줄인다.
113
- const processExpired = store.db.transaction(() => {
112
+ const expireRequests = () => {
114
113
  for (const hr of expired) {
115
114
  store.updateHumanRequest(hr.request_id, 'timed_out', null);
116
115
  if (hr.default_action === 'timeout_continue') {
@@ -127,7 +126,11 @@ export function createHitlManager(store, router = null) {
127
126
  }
128
127
  }
129
128
  return expired.length;
130
- });
129
+ };
130
+
131
+ const processExpired = store.db?.transaction
132
+ ? store.db.transaction(expireRequests)
133
+ : expireRequests;
131
134
 
132
135
  return processExpired();
133
136
  },
package/hub/intent.mjs CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { execFileSync } from 'node:child_process';
5
5
  import crypto from 'node:crypto';
6
+ import { whichCommand } from './platform.mjs';
6
7
 
7
8
  /** 캐시 엔트리: { category, confidence, ts } */
8
9
  const _intentCache = new Map();
@@ -13,13 +14,7 @@ let _codexAvailable = null;
13
14
 
14
15
  function _isCodexAvailable() {
15
16
  if (_codexAvailable !== null) return _codexAvailable;
16
- try {
17
- const cmd = process.platform === 'win32' ? 'where' : 'which';
18
- execFileSync(cmd, ['codex'], { stdio: 'ignore' });
19
- _codexAvailable = true;
20
- } catch {
21
- _codexAvailable = false;
22
- }
17
+ _codexAvailable = Boolean(whichCommand('codex'));
23
18
  return _codexAvailable;
24
19
  }
25
20
 
@@ -5,6 +5,7 @@ import { execSync } from "node:child_process";
5
5
  import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "node:fs";
6
6
  import { homedir, tmpdir } from "node:os";
7
7
  import { join } from "node:path";
8
+ import { IS_WINDOWS, killProcess } from "../platform.mjs";
8
9
 
9
10
  const CLEANUP_SCRIPT_DIR = join(tmpdir(), "tfx-process-utils");
10
11
  const SCAN_SCRIPT_PATH = join(CLEANUP_SCRIPT_DIR, "scan-processes.ps1");
@@ -58,7 +59,7 @@ function killWithEscalation(orphanPids, procMap) {
58
59
  const aliveBeforeKill = new Set(orphanPids.filter(pid => isPidAlive(pid)));
59
60
 
60
61
  for (const pid of aliveBeforeKill) {
61
- try { process.kill(pid, "SIGTERM"); } catch {}
62
+ killProcess(pid, { signal: "SIGTERM" });
62
63
  }
63
64
 
64
65
  sleepSyncMs(3000);
@@ -73,7 +74,7 @@ function killWithEscalation(orphanPids, procMap) {
73
74
  if (snapshot) {
74
75
  try {
75
76
  const current = execSync(
76
- process.platform === "win32"
77
+ IS_WINDOWS
77
78
  ? `powershell -NoProfile -WindowStyle Hidden -Command "(Get-CimInstance Win32_Process -Filter 'ProcessId=${pid}' -ErrorAction SilentlyContinue).ParentProcessId"`
78
79
  : `ps -o ppid= -p ${pid}`,
79
80
  { encoding: "utf8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"], windowsHide: true },
@@ -88,7 +89,7 @@ function killWithEscalation(orphanPids, procMap) {
88
89
  }
89
90
  }
90
91
  }
91
- try { process.kill(pid, "SIGKILL"); } catch {}
92
+ killProcess(pid, { signal: "SIGKILL", force: true });
92
93
  }
93
94
  if (!isPidAlive(pid)) killed++;
94
95
  }
@@ -206,7 +207,7 @@ const KILLABLE_NAMES = new Set([
206
207
  * @returns {{ killed: number, remaining: number }}
207
208
  */
208
209
  export function cleanupOrphanNodeProcesses() {
209
- if (process.platform !== "win32") return cleanupOrphansUnix();
210
+ if (!IS_WINDOWS) return cleanupOrphansUnix();
210
211
 
211
212
  ensureHelperScripts();
212
213
 
package/hub/pipe.mjs CHANGED
@@ -3,7 +3,6 @@
3
3
 
4
4
  import net from 'node:net';
5
5
  import { existsSync, unlinkSync } from 'node:fs';
6
- import { join } from 'node:path';
7
6
  import { randomUUID } from 'node:crypto';
8
7
  import {
9
8
  teamInfo,
@@ -18,16 +17,14 @@ import {
18
17
  listPipelineStates,
19
18
  readPipelineState,
20
19
  } from './pipeline/state.mjs';
20
+ import { IS_WINDOWS, pipePath } from './platform.mjs';
21
21
  import { safeJsonParse } from './workers/worker-utils.mjs';
22
22
 
23
23
  const DEFAULT_HEARTBEAT_TTL_MS = 60000;
24
24
 
25
25
  /** 플랫폼별 pipe 경로 계산 */
26
26
  export function getPipePath(sessionId = process.pid) {
27
- if (process.platform === 'win32') {
28
- return `\\\\.\\pipe\\triflux-${sessionId}`;
29
- }
30
- return join('/tmp', `triflux-${sessionId}.sock`);
27
+ return pipePath('triflux', sessionId);
31
28
  }
32
29
 
33
30
  function normalizeTopics(topics) {
@@ -517,7 +514,7 @@ export function createPipeServer({
517
514
  async start() {
518
515
  if (server) return { path: pipePath };
519
516
 
520
- if (process.platform !== 'win32' && existsSync(pipePath)) {
517
+ if (!IS_WINDOWS && existsSync(pipePath)) {
521
518
  try { unlinkSync(pipePath); } catch {}
522
519
  }
523
520
 
@@ -554,7 +551,7 @@ export function createPipeServer({
554
551
  await new Promise((resolve) => current.close(resolve));
555
552
  }
556
553
 
557
- if (process.platform !== 'win32' && existsSync(pipePath)) {
554
+ if (!IS_WINDOWS && existsSync(pipePath)) {
558
555
  try { unlinkSync(pipePath); } catch {}
559
556
  }
560
557
  },
@@ -0,0 +1,186 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { execFileSync, execSync } from 'node:child_process';
4
+
5
+ export const IS_WINDOWS = process.platform === 'win32';
6
+ export const IS_MAC = process.platform === 'darwin';
7
+ export const IS_LINUX = process.platform === 'linux';
8
+ export const TEMP_DIR = IS_WINDOWS ? os.tmpdir() : '/tmp';
9
+ export const PATH_SEP = path.sep;
10
+
11
+ function getPathApi(platform) {
12
+ return platform === 'win32' ? path.win32 : path.posix;
13
+ }
14
+
15
+ function coercePathInput(value, platform) {
16
+ const text = String(value ?? '');
17
+ if (platform === 'win32') {
18
+ return text.replaceAll('/', '\\');
19
+ }
20
+ return text.replaceAll('\\', '/');
21
+ }
22
+
23
+ function sanitizePipeSegment(value) {
24
+ return String(value ?? '')
25
+ .trim()
26
+ .replace(/[<>:"/\\|?*\u0000-\u001f]+/gu, '-')
27
+ .replace(/-+/g, '-')
28
+ .replace(/^-|-$/g, '');
29
+ }
30
+
31
+ /**
32
+ * 경로를 플랫폼에 맞게 정규화합니다.
33
+ * Windows 환경에서는 역슬래시(\)를 슬래시(/)로 변환하여 반환합니다.
34
+ *
35
+ * @param {string} value - 정규화할 경로 문자열
36
+ * @param {object} [options] - 옵션
37
+ * @param {string} [options.platform] - 대상 플랫폼 (기본값: process.platform)
38
+ * @returns {string} 정규화된 경로
39
+ */
40
+ export function normalizePath(value, options = {}) {
41
+ const platform = options.platform || process.platform;
42
+ const pathApi = getPathApi(platform);
43
+ const normalized = pathApi.normalize(coercePathInput(value, platform));
44
+
45
+ if (platform === 'win32') {
46
+ return normalized.replaceAll('\\', '/');
47
+ }
48
+ return normalized;
49
+ }
50
+
51
+ /**
52
+ * 시스템에서 실행 가능한 명령의 절대 경로를 찾습니다.
53
+ * Windows에서는 'where', Unix 계열에서는 'which' 명령을 사용합니다.
54
+ *
55
+ * @param {string} name - 찾을 명령어 이름
56
+ * @param {object} [options] - 옵션
57
+ * @param {string} [options.platform] - 대상 플랫폼
58
+ * @param {number} [options.timeout=5000] - 검색 타임아웃 (ms)
59
+ * @param {object} [options.env] - 환경 변수
60
+ * @param {string} [options.cwd] - 작업 디렉토리
61
+ * @returns {string|null} 명령어의 절대 경로 또는 찾지 못한 경우 null
62
+ */
63
+ export function whichCommand(name, options = {}) {
64
+ const commandName = String(name ?? '').trim();
65
+ if (!commandName) return null;
66
+
67
+ const platform = options.platform || process.platform;
68
+ const lookupCommand = platform === 'win32' ? 'where' : 'which';
69
+
70
+ try {
71
+ const output = execFileSync(lookupCommand, [commandName], {
72
+ encoding: 'utf8',
73
+ timeout: options.timeout ?? 5000,
74
+ stdio: ['ignore', 'pipe', 'ignore'],
75
+ windowsHide: true,
76
+ env: options.env || process.env,
77
+ cwd: options.cwd,
78
+ });
79
+
80
+ const match = String(output)
81
+ .split(/\r?\n/u)
82
+ .map((line) => line.trim())
83
+ .find(Boolean);
84
+
85
+ return match || null;
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * 프로세스를 종료합니다.
93
+ * Windows에서는 트리 구조 종료(/T) 및 강제 종료(/F)를 지원합니다.
94
+ *
95
+ * @param {number|string} pid - 종료할 프로세스 ID
96
+ * @param {object} [options] - 옵션
97
+ * @param {string} [options.platform] - 대상 플랫폼
98
+ * @param {string} [options.signal='SIGTERM'] - 전송할 신호
99
+ * @param {boolean} [options.tree=false] - 자식 프로세스까지 포함하여 종료할지 여부
100
+ * @param {boolean} [options.force=false] - 강제 종료 여부
101
+ * @param {number} [options.timeout=5000] - 타임아웃 (ms)
102
+ * @returns {boolean} 종료 성공 여부
103
+ */
104
+ export function killProcess(pid, options = {}) {
105
+ const numericPid = Number.parseInt(String(pid), 10);
106
+ if (!Number.isInteger(numericPid) || numericPid <= 0) return false;
107
+
108
+ const platform = options.platform || process.platform;
109
+ const signal = options.signal || 'SIGTERM';
110
+ const tree = options.tree === true;
111
+ const force = options.force === true || signal === 'SIGKILL';
112
+
113
+ try {
114
+ if (platform === 'win32' && (tree || force)) {
115
+ const command = [
116
+ 'taskkill',
117
+ '/PID',
118
+ String(numericPid),
119
+ tree ? '/T' : '',
120
+ force ? '/F' : '',
121
+ ]
122
+ .filter(Boolean)
123
+ .join(' ');
124
+ execSync(command, {
125
+ stdio: 'ignore',
126
+ timeout: options.timeout ?? 5000,
127
+ windowsHide: true,
128
+ });
129
+ return true;
130
+ }
131
+
132
+ process.kill(numericPid, signal);
133
+ return true;
134
+ } catch {
135
+ return false;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * 플랫폼별 IPC 파이프 또는 소켓 경로를 생성합니다.
141
+ * Windows에서는 네임드 파이프 경로를, Unix 계열에서는 도메인 소켓 파일 경로를 반환합니다.
142
+ *
143
+ * @param {string} name - 파이프/소켓 기본 이름
144
+ * @param {number|string} [pid=process.pid] - 프로세스 ID (식별자 추가용)
145
+ * @param {object} [options] - 옵션
146
+ * @param {string} [options.platform] - 대상 플랫폼
147
+ * @param {string} [options.tempDir] - 임시 디렉토리 경로 (Unix 전용)
148
+ * @returns {string} 플랫폼별 파이프/소켓 경로
149
+ */
150
+ export function pipePath(name, pid = process.pid, options = {}) {
151
+ const platform = options.platform || process.platform;
152
+ const safeName = sanitizePipeSegment(name) || 'triflux';
153
+ const suffix = pid == null || pid === '' ? safeName : `${safeName}-${pid}`;
154
+
155
+ if (platform === 'win32') {
156
+ return `\\\\.\\pipe\\${suffix}`;
157
+ }
158
+
159
+ const baseDir = options.tempDir || TEMP_DIR;
160
+ return path.posix.join(baseDir, `${suffix}.sock`);
161
+ }
162
+
163
+ /**
164
+ * 특정 경로가 대상 디렉토리 내부에 포함되는지 확인합니다.
165
+ * 대소문자 구분 및 상대 경로 처리를 플랫폼 규격에 맞게 수행합니다.
166
+ *
167
+ * @param {string} resolvedPath - 검사할 절대 경로
168
+ * @param {string} dir - 기준이 되는 대상 디렉토리 경로
169
+ * @param {object} [options] - 옵션
170
+ * @param {string} [options.platform] - 대상 플랫폼
171
+ * @returns {boolean} 포함 여부
172
+ */
173
+ export function isPathWithin(resolvedPath, dir, options = {}) {
174
+ if (!resolvedPath || !dir) return false;
175
+
176
+ const platform = options.platform || process.platform;
177
+ const pathApi = getPathApi(platform);
178
+ const left = pathApi.resolve(coercePathInput(resolvedPath, platform));
179
+ const right = pathApi.resolve(coercePathInput(dir, platform));
180
+
181
+ const normalizedLeft = platform === 'win32' ? left.toLowerCase() : left;
182
+ const normalizedRight = platform === 'win32' ? right.toLowerCase() : right;
183
+ const relative = pathApi.relative(normalizedRight, normalizedLeft);
184
+
185
+ return relative === '' || (!relative.startsWith('..') && !pathApi.isAbsolute(relative));
186
+ }