triflux 3.2.0-dev.8 → 3.3.0-dev.1
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 +1296 -1055
- package/hooks/hooks.json +17 -0
- package/hooks/keyword-rules.json +20 -4
- package/hooks/pipeline-stop.mjs +54 -0
- package/hub/bridge.mjs +517 -318
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -0
- package/hub/pipeline/index.mjs +121 -0
- package/hub/pipeline/state.mjs +164 -0
- package/hub/pipeline/transitions.mjs +114 -0
- package/hub/router.mjs +422 -161
- package/hub/schema.sql +14 -0
- package/hub/server.mjs +499 -424
- package/hub/store.mjs +388 -314
- package/hub/team/cli-team-common.mjs +348 -0
- package/hub/team/cli-team-control.mjs +393 -0
- package/hub/team/cli-team-start.mjs +516 -0
- package/hub/team/cli-team-status.mjs +269 -0
- package/hub/team/cli.mjs +75 -1475
- package/hub/team/dashboard.mjs +1 -9
- package/hub/team/native.mjs +190 -130
- package/hub/team/nativeProxy.mjs +165 -78
- package/hub/team/orchestrator.mjs +15 -20
- package/hub/team/pane.mjs +137 -103
- package/hub/team/psmux.mjs +506 -0
- package/hub/team/session.mjs +393 -330
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +105 -31
- package/hub/workers/claude-worker.mjs +446 -0
- package/hub/workers/codex-mcp.mjs +414 -0
- package/hub/workers/factory.mjs +18 -0
- package/hub/workers/gemini-worker.mjs +349 -0
- package/hub/workers/interface.mjs +41 -0
- package/hud/hud-qos-status.mjs +1790 -1788
- package/package.json +4 -1
- package/scripts/__tests__/keyword-detector.test.mjs +8 -8
- package/scripts/keyword-detector.mjs +15 -0
- package/scripts/lib/keyword-rules.mjs +4 -1
- package/scripts/preflight-cache.mjs +72 -0
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/setup.mjs +136 -71
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +485 -91
- package/skills/tfx-auto/SKILL.md +90 -564
- package/skills/tfx-auto-codex/SKILL.md +1 -3
- package/skills/tfx-codex/SKILL.md +1 -4
- package/skills/tfx-doctor/SKILL.md +1 -0
- package/skills/tfx-gemini/SKILL.md +1 -4
- package/skills/tfx-multi/SKILL.md +378 -0
- package/skills/tfx-setup/SKILL.md +1 -4
- package/skills/tfx-team/SKILL.md +0 -304
package/hub/team/session.mjs
CHANGED
|
@@ -1,21 +1,33 @@
|
|
|
1
|
-
// hub/team/session.mjs — tmux/wt 세션 생명주기 관리
|
|
1
|
+
// hub/team/session.mjs — tmux/psmux/wt 세션 생명주기 관리
|
|
2
2
|
// 의존성: child_process (Node.js 내장)만 사용
|
|
3
3
|
import { execSync, spawnSync } from "node:child_process";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
import {
|
|
5
|
+
attachPsmuxSession,
|
|
6
|
+
capturePsmuxPane,
|
|
7
|
+
configurePsmuxKeybindings,
|
|
8
|
+
createPsmuxSession,
|
|
9
|
+
getPsmuxSessionAttachedCount,
|
|
10
|
+
hasPsmux,
|
|
11
|
+
killPsmuxSession,
|
|
12
|
+
listPsmuxSessions,
|
|
13
|
+
psmuxExec,
|
|
14
|
+
psmuxSessionExists,
|
|
15
|
+
} from "./psmux.mjs";
|
|
16
|
+
|
|
17
|
+
const GIT_BASH_CANDIDATES = [
|
|
18
|
+
"C:/Program Files/Git/bin/bash.exe",
|
|
19
|
+
"C:/Program Files/Git/usr/bin/bash.exe",
|
|
20
|
+
];
|
|
21
|
+
|
|
10
22
|
function findGitBashExe() {
|
|
11
23
|
for (const p of GIT_BASH_CANDIDATES) {
|
|
12
|
-
try {
|
|
13
|
-
execSync(`"${p}" --version`, { stdio: "ignore", timeout: 3000 });
|
|
14
|
-
return p;
|
|
15
|
-
} catch {
|
|
16
|
-
// 다음 후보
|
|
17
|
-
}
|
|
18
|
-
}
|
|
24
|
+
try {
|
|
25
|
+
execSync(`"${p}" --version`, { stdio: "ignore", timeout: 3000 });
|
|
26
|
+
return p;
|
|
27
|
+
} catch {
|
|
28
|
+
// 다음 후보
|
|
29
|
+
}
|
|
30
|
+
}
|
|
19
31
|
return null;
|
|
20
32
|
}
|
|
21
33
|
|
|
@@ -34,139 +46,154 @@ export function hasWindowsTerminal() {
|
|
|
34
46
|
export function hasWindowsTerminalSession() {
|
|
35
47
|
return process.platform === "win32" && !!process.env.WT_SESSION;
|
|
36
48
|
}
|
|
37
|
-
|
|
38
|
-
/** tmux 실행 가능 여부 확인 */
|
|
39
|
-
function hasTmux() {
|
|
40
|
-
try {
|
|
41
|
-
execSync("tmux -V", { stdio: "ignore", timeout: 3000 });
|
|
42
|
-
return true;
|
|
43
|
-
} catch {
|
|
44
|
-
return false;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** WSL2 내 tmux 사용 가능 여부 (Windows 전용) */
|
|
49
|
-
function hasWslTmux() {
|
|
50
|
-
try {
|
|
51
|
-
execSync("wsl tmux -V", { stdio: "ignore", timeout: 5000 });
|
|
52
|
-
return true;
|
|
53
|
-
} catch {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** Git Bash 내 tmux 사용 가능 여부 (Windows 전용) */
|
|
59
|
-
function hasGitBashTmux() {
|
|
60
|
-
const bash = findGitBashExe();
|
|
61
|
-
if (!bash) return false;
|
|
62
|
-
try {
|
|
63
|
-
const r = spawnSync(bash, ["-lc", "tmux -V"], {
|
|
64
|
-
encoding: "utf8",
|
|
65
|
-
timeout: 5000,
|
|
66
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
67
|
-
});
|
|
68
|
-
return (r.status ?? 1) === 0;
|
|
69
|
-
} catch {
|
|
70
|
-
return false;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* 터미널 멀티플렉서 감지
|
|
76
|
-
* @returns {'tmux'|'git-bash-tmux'|'wsl-tmux'|null}
|
|
77
|
-
*/
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
if (
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
*
|
|
145
|
-
* @
|
|
146
|
-
|
|
147
|
-
|
|
49
|
+
|
|
50
|
+
/** tmux 실행 가능 여부 확인 */
|
|
51
|
+
function hasTmux() {
|
|
52
|
+
try {
|
|
53
|
+
execSync("tmux -V", { stdio: "ignore", timeout: 3000 });
|
|
54
|
+
return true;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** WSL2 내 tmux 사용 가능 여부 (Windows 전용) */
|
|
61
|
+
function hasWslTmux() {
|
|
62
|
+
try {
|
|
63
|
+
execSync("wsl tmux -V", { stdio: "ignore", timeout: 5000 });
|
|
64
|
+
return true;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Git Bash 내 tmux 사용 가능 여부 (Windows 전용) */
|
|
71
|
+
function hasGitBashTmux() {
|
|
72
|
+
const bash = findGitBashExe();
|
|
73
|
+
if (!bash) return false;
|
|
74
|
+
try {
|
|
75
|
+
const r = spawnSync(bash, ["-lc", "tmux -V"], {
|
|
76
|
+
encoding: "utf8",
|
|
77
|
+
timeout: 5000,
|
|
78
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
79
|
+
});
|
|
80
|
+
return (r.status ?? 1) === 0;
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 터미널 멀티플렉서 감지 (결과 캐싱 — 프로세스 수명 동안 불변)
|
|
88
|
+
* @returns {'tmux'|'git-bash-tmux'|'wsl-tmux'|'psmux'|null}
|
|
89
|
+
*/
|
|
90
|
+
let _cachedMux;
|
|
91
|
+
export function detectMultiplexer() {
|
|
92
|
+
if (_cachedMux !== undefined) return _cachedMux;
|
|
93
|
+
if (hasPsmux()) { _cachedMux = "psmux"; return _cachedMux; }
|
|
94
|
+
if (hasTmux()) { _cachedMux = "tmux"; return _cachedMux; }
|
|
95
|
+
if (process.platform === "win32" && hasGitBashTmux()) { _cachedMux = "git-bash-tmux"; return _cachedMux; }
|
|
96
|
+
if (process.platform === "win32" && hasWslTmux()) { _cachedMux = "wsl-tmux"; return _cachedMux; }
|
|
97
|
+
_cachedMux = null;
|
|
98
|
+
return _cachedMux;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* tmux/psmux 커맨드 실행 (wsl-tmux 투명 지원)
|
|
103
|
+
* @param {string} args — tmux 서브커맨드 + 인자
|
|
104
|
+
* @param {object} opts — execSync 옵션
|
|
105
|
+
* @returns {string} stdout
|
|
106
|
+
*/
|
|
107
|
+
function tmux(args, opts = {}) {
|
|
108
|
+
const mux = detectMultiplexer();
|
|
109
|
+
if (!mux) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"tmux/psmux 미발견.\n\n" +
|
|
112
|
+
"tfx multi은 tmux 계열 멀티플렉서가 필요합니다:\n" +
|
|
113
|
+
" Windows: psmux 설치 또는 WSL2 tmux 사용\n" +
|
|
114
|
+
" WSL2: wsl sudo apt install tmux\n" +
|
|
115
|
+
" macOS: brew install tmux\n" +
|
|
116
|
+
" Linux: apt install tmux\n\n" +
|
|
117
|
+
"Windows에서는 WSL2를 권장합니다:\n" +
|
|
118
|
+
" 1. wsl --install\n" +
|
|
119
|
+
" 2. wsl sudo apt install tmux\n" +
|
|
120
|
+
" 3. tfx multi \"작업\" (자동으로 WSL tmux 사용)"
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (mux === "psmux") {
|
|
124
|
+
return psmuxExec(args, opts);
|
|
125
|
+
}
|
|
126
|
+
if (mux === "git-bash-tmux") {
|
|
127
|
+
const bash = findGitBashExe();
|
|
128
|
+
if (!bash) throw new Error("git-bash-tmux 감지 실패");
|
|
129
|
+
const r = spawnSync(bash, ["-lc", `tmux ${args}`], {
|
|
130
|
+
encoding: "utf8",
|
|
131
|
+
timeout: 10000,
|
|
132
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
133
|
+
...opts,
|
|
134
|
+
});
|
|
135
|
+
if ((r.status ?? 1) !== 0) {
|
|
136
|
+
const e = new Error(r.stderr || "tmux command failed");
|
|
137
|
+
e.status = r.status;
|
|
138
|
+
throw e;
|
|
139
|
+
}
|
|
140
|
+
return (r.stdout || "").trim();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const prefix = mux === "wsl-tmux" ? "wsl tmux" : "tmux";
|
|
144
|
+
const result = execSync(`${prefix} ${args}`, {
|
|
145
|
+
encoding: "utf8",
|
|
146
|
+
timeout: 10000,
|
|
147
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
148
|
+
...opts,
|
|
149
|
+
});
|
|
150
|
+
return result != null ? result.trim() : "";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* tmux 명령 직접 실행 (고수준 모듈에서 재사용)
|
|
155
|
+
* @param {string} args
|
|
156
|
+
* @param {object} opts
|
|
157
|
+
* @returns {string}
|
|
158
|
+
*/
|
|
159
|
+
export function tmuxExec(args, opts = {}) {
|
|
160
|
+
return tmux(args, opts);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 현재 멀티플렉서 환경에 맞는 attach 실행 스펙 반환
|
|
165
|
+
* @param {string} sessionName
|
|
166
|
+
* @returns {{ command: string, args: string[] }}
|
|
167
|
+
*/
|
|
148
168
|
export function resolveAttachCommand(sessionName) {
|
|
149
169
|
const mux = detectMultiplexer();
|
|
150
|
-
if (!mux) {
|
|
151
|
-
throw new Error("tmux 미발견");
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (mux === "
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
command:
|
|
166
|
-
args: ["
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
+
if (!mux) {
|
|
171
|
+
throw new Error("tmux/psmux 미발견");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (mux === "psmux") {
|
|
175
|
+
return {
|
|
176
|
+
command: process.env.PSMUX_BIN || "psmux",
|
|
177
|
+
args: ["attach-session", "-t", sessionName],
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (mux === "git-bash-tmux") {
|
|
182
|
+
const bash = findGitBashExe();
|
|
183
|
+
if (!bash) throw new Error("git-bash-tmux 감지 실패");
|
|
184
|
+
return {
|
|
185
|
+
command: bash,
|
|
186
|
+
args: ["-lc", `tmux attach-session -t ${sessionName}`],
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (mux === "wsl-tmux") {
|
|
191
|
+
return {
|
|
192
|
+
command: "wsl",
|
|
193
|
+
args: ["tmux", "attach-session", "-t", sessionName],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
170
197
|
return {
|
|
171
198
|
command: "tmux",
|
|
172
199
|
args: ["attach-session", "-t", sessionName],
|
|
@@ -319,80 +346,85 @@ export function closeWtSession(opts = {}) {
|
|
|
319
346
|
|
|
320
347
|
return closed;
|
|
321
348
|
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* tmux 세션 생성 + 레이아웃 분할
|
|
325
|
-
* @param {string} sessionName — 세션 이름
|
|
326
|
-
* @param {object} opts
|
|
327
|
-
* @param {'2x2'|'1xN'|'Nx1'} opts.layout — 레이아웃 (기본 2x2)
|
|
328
|
-
* @param {number} opts.paneCount — pane 수 (기본 4)
|
|
329
|
-
* @returns {{ sessionName: string, panes: string[] }}
|
|
330
|
-
*/
|
|
331
|
-
export function createSession(sessionName, opts = {}) {
|
|
332
|
-
const { layout = "2x2", paneCount = 4 } = opts;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
for (let i =
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
for (let i =
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
for (let i =
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
for (let i =
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
}
|
|
395
|
-
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* tmux 세션 생성 + 레이아웃 분할
|
|
352
|
+
* @param {string} sessionName — 세션 이름
|
|
353
|
+
* @param {object} opts
|
|
354
|
+
* @param {'2x2'|'1xN'|'Nx1'} opts.layout — 레이아웃 (기본 2x2)
|
|
355
|
+
* @param {number} opts.paneCount — pane 수 (기본 4)
|
|
356
|
+
* @returns {{ sessionName: string, panes: string[] }}
|
|
357
|
+
*/
|
|
358
|
+
export function createSession(sessionName, opts = {}) {
|
|
359
|
+
const { layout = "2x2", paneCount = 4 } = opts;
|
|
360
|
+
const mux = detectMultiplexer();
|
|
361
|
+
|
|
362
|
+
// 기존 세션 정리
|
|
363
|
+
if (sessionExists(sessionName)) {
|
|
364
|
+
killSession(sessionName);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (mux === "psmux") {
|
|
368
|
+
return createPsmuxSession(sessionName, { layout, paneCount });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// 새 세션 생성 (detached)
|
|
372
|
+
tmux(`new-session -d -s ${sessionName} -x 220 -y 55`);
|
|
373
|
+
|
|
374
|
+
const panes = [`${sessionName}:0.0`];
|
|
375
|
+
|
|
376
|
+
if (layout === "2x2" && paneCount >= 3) {
|
|
377
|
+
// 3-pane 기본: lead 왼쪽, workers 오른쪽 상/하
|
|
378
|
+
// 4-pane: 좌/우 각각 상/하(균등 2x2)
|
|
379
|
+
tmux(`split-window -h -t ${sessionName}:0.0`);
|
|
380
|
+
tmux(`split-window -v -t ${sessionName}:0.1`);
|
|
381
|
+
if (paneCount >= 4) {
|
|
382
|
+
tmux(`split-window -v -t ${sessionName}:0.0`);
|
|
383
|
+
}
|
|
384
|
+
// pane ID 재수집
|
|
385
|
+
panes.length = 0;
|
|
386
|
+
for (let i = 0; i < Math.min(paneCount, 4); i++) {
|
|
387
|
+
panes.push(`${sessionName}:0.${i}`);
|
|
388
|
+
}
|
|
389
|
+
} else if (layout === "1xN") {
|
|
390
|
+
// 세로 분할(좌/우 컬럼 확장)
|
|
391
|
+
for (let i = 1; i < paneCount; i++) {
|
|
392
|
+
tmux(`split-window -h -t ${sessionName}:0`);
|
|
393
|
+
}
|
|
394
|
+
tmux(`select-layout -t ${sessionName}:0 even-horizontal`);
|
|
395
|
+
panes.length = 0;
|
|
396
|
+
for (let i = 0; i < paneCount; i++) {
|
|
397
|
+
panes.push(`${sessionName}:0.${i}`);
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
// Nx1 가로 분할(상/하 스택)
|
|
401
|
+
for (let i = 1; i < paneCount; i++) {
|
|
402
|
+
tmux(`split-window -v -t ${sessionName}:0`);
|
|
403
|
+
}
|
|
404
|
+
tmux(`select-layout -t ${sessionName}:0 even-vertical`);
|
|
405
|
+
panes.length = 0;
|
|
406
|
+
for (let i = 0; i < paneCount; i++) {
|
|
407
|
+
panes.push(`${sessionName}:0.${i}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return { sessionName, panes };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* pane 포커스 이동
|
|
416
|
+
* @param {string} target
|
|
417
|
+
* @param {object} opts
|
|
418
|
+
* @param {boolean} opts.zoom
|
|
419
|
+
*/
|
|
420
|
+
export function focusPane(target, opts = {}) {
|
|
421
|
+
const { zoom = false } = opts;
|
|
422
|
+
tmux(`select-pane -t ${target}`);
|
|
423
|
+
if (zoom) {
|
|
424
|
+
try { tmux(`resize-pane -t ${target} -Z`); } catch {}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
396
428
|
/**
|
|
397
429
|
* 팀메이트 조작 키 바인딩 설정
|
|
398
430
|
* - Shift+Down: 다음 팀메이트
|
|
@@ -402,11 +434,16 @@ export function focusPane(target, opts = {}) {
|
|
|
402
434
|
* - Escape: 현재 팀메이트 인터럽트(C-c)
|
|
403
435
|
* - Ctrl+T: 태스크 목록 표시
|
|
404
436
|
* @param {string} sessionName
|
|
405
|
-
* @param {object} opts
|
|
406
|
-
* @param {boolean} opts.inProcess
|
|
407
|
-
* @param {string} opts.taskListCommand
|
|
408
|
-
*/
|
|
437
|
+
* @param {object} opts
|
|
438
|
+
* @param {boolean} opts.inProcess
|
|
439
|
+
* @param {string} opts.taskListCommand
|
|
440
|
+
*/
|
|
409
441
|
export function configureTeammateKeybindings(sessionName, opts = {}) {
|
|
442
|
+
if (detectMultiplexer() === "psmux") {
|
|
443
|
+
configurePsmuxKeybindings(sessionName, opts);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
410
447
|
const { inProcess = false, taskListCommand = "" } = opts;
|
|
411
448
|
const cond = `#{==:#{session_name},${sessionName}}`;
|
|
412
449
|
|
|
@@ -436,110 +473,136 @@ export function configureTeammateKeybindings(sessionName, opts = {}) {
|
|
|
436
473
|
|
|
437
474
|
// 현재 활성 pane 인터럽트
|
|
438
475
|
tmux(`bind-key -T root -n Escape if-shell -F '${cond}' 'send-keys C-c' 'send-keys Escape'`);
|
|
439
|
-
|
|
440
|
-
// 태스크 목록 토글 (tmux 3.2+ popup 우선, 실패 시 안내 메시지)
|
|
441
|
-
if (taskListCommand) {
|
|
442
|
-
const escaped = taskListCommand.replace(/'/g, "'\\''");
|
|
443
|
-
try {
|
|
444
|
-
tmux(`bind-key -T root -n C-t if-shell -F '${cond}' \"display-popup -E '${escaped}'\" \"send-keys C-t\"`);
|
|
445
|
-
} catch {
|
|
446
|
-
tmux(`bind-key -T root -n C-t if-shell -F '${cond}' 'display-message "tfx
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* tmux 세션 연결 (포그라운드 전환)
|
|
453
|
-
* @param {string} sessionName
|
|
454
|
-
*/
|
|
455
|
-
export function attachSession(sessionName) {
|
|
456
|
-
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
457
|
-
throw new Error("현재 터미널은 tmux attach를 지원하지 않음 (non-TTY)");
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
*
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
*
|
|
532
|
-
* @
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
476
|
+
|
|
477
|
+
// 태스크 목록 토글 (tmux 3.2+ popup 우선, 실패 시 안내 메시지)
|
|
478
|
+
if (taskListCommand) {
|
|
479
|
+
const escaped = taskListCommand.replace(/'/g, "'\\''");
|
|
480
|
+
try {
|
|
481
|
+
tmux(`bind-key -T root -n C-t if-shell -F '${cond}' \"display-popup -E '${escaped}'\" \"send-keys C-t\"`);
|
|
482
|
+
} catch {
|
|
483
|
+
tmux(`bind-key -T root -n C-t if-shell -F '${cond}' 'display-message "tfx multi tasks 명령으로 태스크 확인"' 'send-keys C-t'`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* tmux 세션 연결 (포그라운드 전환)
|
|
490
|
+
* @param {string} sessionName
|
|
491
|
+
*/
|
|
492
|
+
export function attachSession(sessionName) {
|
|
493
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
494
|
+
throw new Error("현재 터미널은 tmux attach를 지원하지 않음 (non-TTY)");
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (detectMultiplexer() === "psmux") {
|
|
498
|
+
attachPsmuxSession(sessionName);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const { command, args } = resolveAttachCommand(sessionName);
|
|
503
|
+
const r = spawnSync(command, args, {
|
|
504
|
+
stdio: "inherit",
|
|
505
|
+
timeout: 0, // 타임아웃 없음 (사용자가 detach할 때까지)
|
|
506
|
+
});
|
|
507
|
+
if ((r.status ?? 1) !== 0) {
|
|
508
|
+
throw new Error(`tmux attach 실패 (exit=${r.status})`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* tmux 세션 존재 확인
|
|
514
|
+
* @param {string} sessionName
|
|
515
|
+
* @returns {boolean}
|
|
516
|
+
*/
|
|
517
|
+
export function sessionExists(sessionName) {
|
|
518
|
+
if (detectMultiplexer() === "psmux") {
|
|
519
|
+
return psmuxSessionExists(sessionName);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
tmux(`has-session -t ${sessionName}`, { stdio: "ignore" });
|
|
524
|
+
return true;
|
|
525
|
+
} catch {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* tmux 세션 종료
|
|
532
|
+
* @param {string} sessionName
|
|
533
|
+
*/
|
|
534
|
+
export function killSession(sessionName) {
|
|
535
|
+
if (detectMultiplexer() === "psmux") {
|
|
536
|
+
killPsmuxSession(sessionName);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
tmux(`kill-session -t ${sessionName}`, { stdio: "ignore" });
|
|
542
|
+
} catch {
|
|
543
|
+
// 이미 종료된 세션 — 무시
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* tfx-multi- 접두사 세션 목록
|
|
549
|
+
* @returns {string[]}
|
|
550
|
+
*/
|
|
551
|
+
export function listSessions() {
|
|
552
|
+
if (detectMultiplexer() === "psmux") {
|
|
553
|
+
return listPsmuxSessions();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
const output = tmux('list-sessions -F "#{session_name}"');
|
|
558
|
+
return output
|
|
559
|
+
.split("\n")
|
|
560
|
+
.filter((s) => s.startsWith("tfx-multi-"));
|
|
561
|
+
} catch {
|
|
562
|
+
return [];
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* 세션 attach client 수 조회
|
|
568
|
+
* @param {string} sessionName
|
|
569
|
+
* @returns {number|null}
|
|
570
|
+
*/
|
|
571
|
+
export function getSessionAttachedCount(sessionName) {
|
|
572
|
+
if (detectMultiplexer() === "psmux") {
|
|
573
|
+
return getPsmuxSessionAttachedCount(sessionName);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
const output = tmux('list-sessions -F "#{session_name} #{session_attached}"');
|
|
578
|
+
const line = output
|
|
579
|
+
.split("\n")
|
|
580
|
+
.find((l) => l.startsWith(`${sessionName} `));
|
|
581
|
+
if (!line) return null;
|
|
582
|
+
const n = parseInt(line.split(" ")[1], 10);
|
|
583
|
+
return Number.isFinite(n) ? n : null;
|
|
584
|
+
} catch {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* pane 마지막 N줄 캡처
|
|
591
|
+
* @param {string} target — 예: tfx-multi-abc:0.1
|
|
592
|
+
* @param {number} lines — 캡처할 줄 수 (기본 5)
|
|
593
|
+
* @returns {string}
|
|
594
|
+
*/
|
|
595
|
+
export function capturePaneOutput(target, lines = 5) {
|
|
596
|
+
if (detectMultiplexer() === "psmux") {
|
|
597
|
+
return capturePsmuxPane(target, lines);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
// -l 플래그는 일부 tmux 빌드(MSYS2)에서 미지원 → 전체 캡처 후 JS에서 절삭
|
|
602
|
+
const full = tmux(`capture-pane -t ${target} -p`);
|
|
603
|
+
const nonEmpty = full.split("\n").filter((l) => l.trim() !== "");
|
|
604
|
+
return nonEmpty.slice(-lines).join("\n");
|
|
605
|
+
} catch {
|
|
606
|
+
return "";
|
|
607
|
+
}
|
|
608
|
+
}
|