@triflux/remote 10.53.0 → 10.55.0
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/hub/team/backend.mjs +10 -3
- package/hub/team/cli/commands/start/index.mjs +0 -2
- package/hub/team/handoff.mjs +1 -1
- package/hub/team/headless.mjs +41 -57
- package/hub/team/intervention.mjs +6 -4
- package/hub/team/nativeProxy.mjs +2 -48
- package/hub/team/pane.mjs +31 -18
- package/hub/team/psmux.mjs +36 -20
- package/hub/team/remote-session.mjs +66 -56
- package/hub/team/retry-state-machine.mjs +1 -1
- package/hub/team/session.mjs +80 -59
- package/hub/team/terminal-opener.mjs +5 -19
- package/hub/team/tui-viewer.mjs +2 -2
- package/hub/team/tui.mjs +0 -6
- package/hub/workers/factory.mjs +1 -10
- package/hub/workers/interface.mjs +0 -7
- package/package.json +2 -6
- package/scripts/lib/backup-rotation.mjs +60 -0
- package/scripts/lib/codex-profile-config.mjs +20 -7
- package/scripts/lib/codex-recovery.sh +2 -3
- package/scripts/lib/install-blob-history.json +3196 -0
- package/scripts/lib/install-retire.mjs +203 -0
- package/scripts/lib/legacy-hook-cleanup.mjs +148 -13
- package/scripts/lib/legacy-mcp-cleanup.mjs +405 -104
- package/scripts/lib/machine-profile.mjs +2 -2
- package/scripts/lib/mcp-filter.mjs +1 -42
- package/scripts/lib/mcp-guard-engine.mjs +198 -19
- package/scripts/lib/mcp-manifest.mjs +0 -40
- package/scripts/lib/process-utils.mjs +19 -0
- package/scripts/lib/stable-node.mjs +35 -0
- package/scripts/lib/test-env.mjs +20 -0
- package/cto/brief.mjs +0 -73
- package/cto/collect.mjs +0 -490
- package/cto/current.schema.json +0 -148
- package/cto/events.mjs +0 -208
- package/cto/hygiene.mjs +0 -320
- package/cto/lake-root.mjs +0 -127
- package/cto/status.mjs +0 -124
- package/hub/lib/cto-env.mjs +0 -1
- package/hub/team/notify.mjs +0 -385
- package/hub/team/process-cleanup.mjs +0 -386
- package/hub/team/routing.mjs +0 -249
- package/hub/team/staleState.mjs +0 -403
- package/hub/team/worker-signal.mjs +0 -263
- package/hub/team/worker-signal.types.d.ts +0 -52
- package/hub/workers/codex-mcp.mjs +0 -853
- package/scripts/lib/remote-spawn-transfer.mjs +0 -226
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
6
7
|
import {
|
|
7
8
|
basename,
|
|
9
|
+
isAbsolute,
|
|
8
10
|
join,
|
|
9
11
|
posix as posixPath,
|
|
10
12
|
win32 as win32Path,
|
|
@@ -21,12 +23,25 @@ export function shellQuote(value) {
|
|
|
21
23
|
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
22
24
|
}
|
|
23
25
|
|
|
26
|
+
// PowerShell 은 ‘ ’ ‚ ‛ 도 작은따옴표로 읽는다. 따옴표 문자를 모두 겹쳐 써야 인용이 안 끝난다.
|
|
24
27
|
export function escapePwshSingleQuoted(value) {
|
|
25
|
-
return String(value).replace(/'/g, "
|
|
28
|
+
return String(value).replace(/['\u2018\u2019\u201A\u201B]/g, "$&$&");
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
// 큰따옴표 안에서는 $ 가 확장되고 “ ” „ 도 따옴표로 읽힌다. 모두 백틱으로 막는다.
|
|
28
32
|
export function escapePwshDoubleQuoted(value) {
|
|
29
|
-
return String(value).replace(
|
|
33
|
+
return String(value).replace(/[`$"\u201C\u201D\u201E]/g, "`$&");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ssh 는 원격 명령 인자를 공백으로 이어 원격 셸에 다시 넘긴다. 스크립트는 인용한 문자열 하나로 만든다.
|
|
37
|
+
export function posixRemoteCommand(script) {
|
|
38
|
+
return `sh -lc ${shellQuote(script)}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// pwsh 스크립트는 base64 로 넘겨 원격 기본 셸(cmd 나 pwsh)이 따옴표와 파이프를 해석하지 않게 한다.
|
|
42
|
+
export function pwshRemoteCommand(script) {
|
|
43
|
+
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
|
44
|
+
return `pwsh -NoProfile -EncodedCommand ${encoded}`;
|
|
30
45
|
}
|
|
31
46
|
|
|
32
47
|
function normalizeCommandPath(value) {
|
|
@@ -146,6 +161,24 @@ function probeRemoteEnvViaPosix(host) {
|
|
|
146
161
|
|
|
147
162
|
// ── Cache ───────────────────────────────────────────────────────
|
|
148
163
|
|
|
164
|
+
// cwd 의 .omc 에 두면 실행한 저장소마다 원격 홈 경로가 흩어져 남는다. 사용자 상태 경로에 둔다.
|
|
165
|
+
export function tfxStateDir(env = process.env) {
|
|
166
|
+
// XDG 명세대로 상대 경로는 무시한다. 받아들이면 캐시가 다시 cwd 아래로 간다.
|
|
167
|
+
const xdgState = isAbsolute(env.XDG_STATE_HOME || "")
|
|
168
|
+
? env.XDG_STATE_HOME
|
|
169
|
+
: "";
|
|
170
|
+
const stateRoot =
|
|
171
|
+
xdgState ||
|
|
172
|
+
(process.platform === "win32"
|
|
173
|
+
? env.LOCALAPPDATA || join(homedir(), "AppData", "Local")
|
|
174
|
+
: join(homedir(), ".local", "state"));
|
|
175
|
+
return join(stateRoot, "triflux");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function remoteEnvCacheDir(env = process.env) {
|
|
179
|
+
return join(tfxStateDir(env), "remote-env");
|
|
180
|
+
}
|
|
181
|
+
|
|
149
182
|
function getEnvCachePath(host, cacheDir) {
|
|
150
183
|
return join(cacheDir, `${host}.json`);
|
|
151
184
|
}
|
|
@@ -161,13 +194,11 @@ function readEnvCache(host, cacheDir) {
|
|
|
161
194
|
}
|
|
162
195
|
}
|
|
163
196
|
|
|
164
|
-
function isEnvCacheFresh(entry) {
|
|
165
|
-
return
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
Date.now() - entry.cachedAt < REMOTE_ENV_TTL_MS,
|
|
170
|
-
);
|
|
197
|
+
export function isEnvCacheFresh(entry, now = Date.now()) {
|
|
198
|
+
if (!entry?.env || typeof entry.cachedAt !== "number") return false;
|
|
199
|
+
const age = now - entry.cachedAt;
|
|
200
|
+
// 미래 시각을 허용하면 그 캐시가 영원히 신선하게 남는다.
|
|
201
|
+
return age >= 0 && age < REMOTE_ENV_TTL_MS;
|
|
171
202
|
}
|
|
172
203
|
|
|
173
204
|
function writeEnvCache(host, env, cacheDir) {
|
|
@@ -186,13 +217,13 @@ function writeEnvCache(host, env, cacheDir) {
|
|
|
186
217
|
* @param {string} host — SSH host
|
|
187
218
|
* @param {object} [opts]
|
|
188
219
|
* @param {boolean} [opts.force=false] — bypass cache
|
|
189
|
-
* @param {string} [opts.cacheDir]
|
|
220
|
+
* @param {string} [opts.cacheDir]: cache directory (default: remoteEnvCacheDir())
|
|
190
221
|
* @returns {Readonly<RemoteEnv>}
|
|
191
222
|
*/
|
|
192
223
|
export function probeRemoteEnv(host, opts = {}) {
|
|
193
224
|
validateHost(host);
|
|
194
225
|
const force = opts.force === true;
|
|
195
|
-
const cacheDir = opts.cacheDir ||
|
|
226
|
+
const cacheDir = opts.cacheDir || remoteEnvCacheDir();
|
|
196
227
|
|
|
197
228
|
if (!force) {
|
|
198
229
|
const cached = readEnvCache(host, cacheDir);
|
|
@@ -266,26 +297,13 @@ export function resolveRemoteStageDir(env, stageId) {
|
|
|
266
297
|
* @param {string} remoteStageDir
|
|
267
298
|
*/
|
|
268
299
|
export function ensureRemoteStageDir(host, env, remoteStageDir) {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
"-NoProfile",
|
|
277
|
-
"-Command",
|
|
278
|
-
`New-Item -ItemType Directory -Path '${safePath}' -Force | Out-Null`,
|
|
279
|
-
],
|
|
280
|
-
{ timeout: 10000, stdio: "pipe" },
|
|
281
|
-
);
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
execFileSync(
|
|
285
|
-
"ssh",
|
|
286
|
-
[host, "sh", "-lc", `mkdir -p ${shellQuote(remoteStageDir)}`],
|
|
287
|
-
{ timeout: 10000, stdio: "pipe" },
|
|
288
|
-
);
|
|
300
|
+
const command =
|
|
301
|
+
env.os === "win32"
|
|
302
|
+
? pwshRemoteCommand(
|
|
303
|
+
`New-Item -ItemType Directory -Path '${escapePwshSingleQuoted(remoteStageDir)}' -Force | Out-Null`,
|
|
304
|
+
)
|
|
305
|
+
: posixRemoteCommand(`mkdir -p ${shellQuote(remoteStageDir)}`);
|
|
306
|
+
execFileSync("ssh", [host, command], { timeout: 10000, stdio: "pipe" });
|
|
289
307
|
}
|
|
290
308
|
|
|
291
309
|
/**
|
|
@@ -342,29 +360,21 @@ export function stageRemotePromptFiles(host, env, transferCandidates, stageId) {
|
|
|
342
360
|
* @returns {string} stdout
|
|
343
361
|
*/
|
|
344
362
|
export function remoteGit(host, env, gitArgs, cwd) {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
"ssh",
|
|
363
|
-
[host, "sh", "-lc", `cd ${shellQuote(cwd)} && ${gitCmd}`],
|
|
364
|
-
{
|
|
365
|
-
encoding: "utf8",
|
|
366
|
-
timeout: 30_000,
|
|
367
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
368
|
-
},
|
|
369
|
-
).trim();
|
|
363
|
+
const command =
|
|
364
|
+
env.os === "win32"
|
|
365
|
+
? pwshRemoteCommand(
|
|
366
|
+
[
|
|
367
|
+
`Set-Location '${escapePwshSingleQuoted(cwd)}';`,
|
|
368
|
+
"git",
|
|
369
|
+
...gitArgs.map((arg) => `'${escapePwshSingleQuoted(arg)}'`),
|
|
370
|
+
].join(" "),
|
|
371
|
+
)
|
|
372
|
+
: posixRemoteCommand(
|
|
373
|
+
`cd ${shellQuote(cwd)} && ${["git", ...gitArgs].map(shellQuote).join(" ")}`,
|
|
374
|
+
);
|
|
375
|
+
return execFileSync("ssh", [host, command], {
|
|
376
|
+
encoding: "utf8",
|
|
377
|
+
timeout: 30_000,
|
|
378
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
379
|
+
}).trim();
|
|
370
380
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// hub/team/retry-state-machine.mjs
|
|
2
2
|
// Phase 3 Step A — true ralph / auto-escalate retry state machine.
|
|
3
|
-
//
|
|
3
|
+
// 설계: 구현 커밋 565a2bad 메시지, 규칙은 .claude/rules/tfx-escalation-chain.md
|
|
4
4
|
//
|
|
5
5
|
// Modes:
|
|
6
6
|
// bounded — --retry 1 (기본): maxIterations 도달 시 BUDGET_EXCEEDED.
|
package/hub/team/session.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// tmux/psmux 세션 생명주기 관리
|
|
2
|
-
import {
|
|
2
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
3
3
|
import { resolveGitBashExecutable } from "@triflux/core/hub/lib/bash-path.mjs";
|
|
4
4
|
import { getEnvironment } from "@triflux/core/hub/lib/env-detect.mjs";
|
|
5
5
|
import {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
listPsmuxSessions,
|
|
15
15
|
psmuxExec,
|
|
16
16
|
psmuxSessionExists,
|
|
17
|
+
tokenizeCommand,
|
|
17
18
|
} from "./psmux.mjs";
|
|
18
19
|
|
|
19
20
|
/** Windows Terminal 실행 파일 존재 여부 */
|
|
@@ -29,7 +30,11 @@ export function hasWindowsTerminalSession() {
|
|
|
29
30
|
/** tmux 실행 가능 여부 확인 */
|
|
30
31
|
function hasTmux() {
|
|
31
32
|
try {
|
|
32
|
-
|
|
33
|
+
execFileSync("tmux", ["-V"], {
|
|
34
|
+
stdio: "ignore",
|
|
35
|
+
timeout: 3000,
|
|
36
|
+
windowsHide: true,
|
|
37
|
+
});
|
|
33
38
|
return true;
|
|
34
39
|
} catch {
|
|
35
40
|
return false;
|
|
@@ -100,10 +105,16 @@ export function detectMultiplexer() {
|
|
|
100
105
|
return _cachedMux;
|
|
101
106
|
}
|
|
102
107
|
|
|
108
|
+
function bashArg(value) {
|
|
109
|
+
return /^[\w@%+=:,./-]+$/u.test(value)
|
|
110
|
+
? value
|
|
111
|
+
: `'${value.replace(/'/g, "'\\''")}'`;
|
|
112
|
+
}
|
|
113
|
+
|
|
103
114
|
/**
|
|
104
115
|
* tmux/psmux 커맨드 실행
|
|
105
|
-
* @param {string} args
|
|
106
|
-
* @param {object} opts
|
|
116
|
+
* @param {string[]|string} args: tmux 서브커맨드 + 인자. 문자열은 psmux 와 같은 규칙으로 쪼갠다.
|
|
117
|
+
* @param {object} opts: execFileSync 옵션
|
|
107
118
|
* @returns {string} stdout
|
|
108
119
|
*/
|
|
109
120
|
function tmux(args, opts = {}) {
|
|
@@ -120,16 +131,22 @@ function tmux(args, opts = {}) {
|
|
|
120
131
|
if (mux === "psmux") {
|
|
121
132
|
return psmuxExec(args, opts);
|
|
122
133
|
}
|
|
134
|
+
// 셸을 거치지 않게 인자 배열로 넘긴다. 세션 이름이나 pane 대상이 셸 문법으로 읽히지 않는다.
|
|
135
|
+
const argv = Array.isArray(args) ? args.map(String) : tokenizeCommand(args);
|
|
123
136
|
if (mux === "git-bash-tmux") {
|
|
124
137
|
const bash = resolveGitBashExecutable();
|
|
125
138
|
if (!bash) throw new Error("git-bash-tmux 감지 실패");
|
|
126
|
-
const r = spawnSync(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
139
|
+
const r = spawnSync(
|
|
140
|
+
bash,
|
|
141
|
+
["-lc", ["tmux", ...argv.map(bashArg)].join(" ")],
|
|
142
|
+
{
|
|
143
|
+
encoding: "utf8",
|
|
144
|
+
timeout: 10000,
|
|
145
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
146
|
+
windowsHide: true,
|
|
147
|
+
...opts,
|
|
148
|
+
},
|
|
149
|
+
);
|
|
133
150
|
if ((r.status ?? 1) !== 0) {
|
|
134
151
|
const e = new Error(r.stderr || "tmux command failed");
|
|
135
152
|
e.status = r.status;
|
|
@@ -138,7 +155,7 @@ function tmux(args, opts = {}) {
|
|
|
138
155
|
return (r.stdout || "").trim();
|
|
139
156
|
}
|
|
140
157
|
|
|
141
|
-
const result =
|
|
158
|
+
const result = execFileSync("tmux", argv, {
|
|
142
159
|
encoding: "utf8",
|
|
143
160
|
timeout: 10000,
|
|
144
161
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -150,7 +167,7 @@ function tmux(args, opts = {}) {
|
|
|
150
167
|
|
|
151
168
|
/**
|
|
152
169
|
* tmux 명령 직접 실행 (고수준 모듈에서 재사용)
|
|
153
|
-
* @param {string} args
|
|
170
|
+
* @param {string[]|string} args
|
|
154
171
|
* @param {object} opts
|
|
155
172
|
* @returns {string}
|
|
156
173
|
*/
|
|
@@ -181,7 +198,7 @@ export function resolveAttachCommand(sessionName, opts = {}) {
|
|
|
181
198
|
if (!bash) throw new Error("git-bash-tmux 감지 실패");
|
|
182
199
|
return {
|
|
183
200
|
command: bash,
|
|
184
|
-
args: ["-lc", `tmux attach-session -t ${sessionName}`],
|
|
201
|
+
args: ["-lc", `tmux attach-session -t ${bashArg(sessionName)}`],
|
|
185
202
|
};
|
|
186
203
|
}
|
|
187
204
|
|
|
@@ -213,17 +230,17 @@ export function createSession(sessionName, opts = {}) {
|
|
|
213
230
|
}
|
|
214
231
|
|
|
215
232
|
// 새 세션 생성 (detached)
|
|
216
|
-
tmux(
|
|
233
|
+
tmux(["new-session", "-d", "-s", sessionName, "-x", "220", "-y", "55"]);
|
|
217
234
|
|
|
218
235
|
const panes = [`${sessionName}:0.0`];
|
|
219
236
|
|
|
220
237
|
if (layout === "2x2" && paneCount >= 3) {
|
|
221
238
|
// 3-pane 기본: lead 왼쪽, workers 오른쪽 상/하
|
|
222
239
|
// 4-pane: 좌/우 각각 상/하(균등 2x2)
|
|
223
|
-
tmux(
|
|
224
|
-
tmux(
|
|
240
|
+
tmux(["split-window", "-h", "-t", `${sessionName}:0.0`]);
|
|
241
|
+
tmux(["split-window", "-v", "-t", `${sessionName}:0.1`]);
|
|
225
242
|
if (paneCount >= 4) {
|
|
226
|
-
tmux(
|
|
243
|
+
tmux(["split-window", "-v", "-t", `${sessionName}:0.0`]);
|
|
227
244
|
}
|
|
228
245
|
// pane ID 재수집
|
|
229
246
|
panes.length = 0;
|
|
@@ -233,9 +250,9 @@ export function createSession(sessionName, opts = {}) {
|
|
|
233
250
|
} else if (layout === "1xN") {
|
|
234
251
|
// 세로 분할(좌/우 컬럼 확장)
|
|
235
252
|
for (let i = 1; i < paneCount; i++) {
|
|
236
|
-
tmux(
|
|
253
|
+
tmux(["split-window", "-h", "-t", `${sessionName}:0`]);
|
|
237
254
|
}
|
|
238
|
-
tmux(
|
|
255
|
+
tmux(["select-layout", "-t", `${sessionName}:0`, "even-horizontal"]);
|
|
239
256
|
panes.length = 0;
|
|
240
257
|
for (let i = 0; i < paneCount; i++) {
|
|
241
258
|
panes.push(`${sessionName}:0.${i}`);
|
|
@@ -243,9 +260,9 @@ export function createSession(sessionName, opts = {}) {
|
|
|
243
260
|
} else {
|
|
244
261
|
// Nx1 가로 분할(상/하 스택)
|
|
245
262
|
for (let i = 1; i < paneCount; i++) {
|
|
246
|
-
tmux(
|
|
263
|
+
tmux(["split-window", "-v", "-t", `${sessionName}:0`]);
|
|
247
264
|
}
|
|
248
|
-
tmux(
|
|
265
|
+
tmux(["select-layout", "-t", `${sessionName}:0`, "even-vertical"]);
|
|
249
266
|
panes.length = 0;
|
|
250
267
|
for (let i = 0; i < paneCount; i++) {
|
|
251
268
|
panes.push(`${sessionName}:0.${i}`);
|
|
@@ -263,10 +280,10 @@ export function createSession(sessionName, opts = {}) {
|
|
|
263
280
|
*/
|
|
264
281
|
export function focusPane(target, opts = {}) {
|
|
265
282
|
const { zoom = false } = opts;
|
|
266
|
-
tmux(
|
|
283
|
+
tmux(["select-pane", "-t", target]);
|
|
267
284
|
if (zoom) {
|
|
268
285
|
try {
|
|
269
|
-
tmux(
|
|
286
|
+
tmux(["resize-pane", "-t", target, "-Z"]);
|
|
270
287
|
} catch {}
|
|
271
288
|
}
|
|
272
289
|
}
|
|
@@ -296,45 +313,46 @@ export function configureTeammateKeybindings(sessionName, opts = {}) {
|
|
|
296
313
|
// Shift+Up이 터미널/호스트 조합에 따라 전달되지 않는 경우가 있어
|
|
297
314
|
// 좌/우/Shift+Tab 대체 키를 함께 바인딩한다.
|
|
298
315
|
const bindNext = inProcess
|
|
299
|
-
?
|
|
300
|
-
:
|
|
316
|
+
? "select-pane -t :.+ \\; resize-pane -Z"
|
|
317
|
+
: "select-pane -t :.+";
|
|
301
318
|
const bindPrev = inProcess
|
|
302
|
-
?
|
|
303
|
-
:
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
319
|
+
? "select-pane -t :.- \\; resize-pane -Z"
|
|
320
|
+
: "select-pane -t :.-";
|
|
321
|
+
const bindIf = (key, onMatch, otherwise) =>
|
|
322
|
+
tmux([
|
|
323
|
+
"bind-key",
|
|
324
|
+
"-T",
|
|
325
|
+
"root",
|
|
326
|
+
"-n",
|
|
327
|
+
key,
|
|
328
|
+
"if-shell",
|
|
329
|
+
"-F",
|
|
330
|
+
cond,
|
|
331
|
+
onMatch,
|
|
332
|
+
otherwise,
|
|
333
|
+
]);
|
|
334
|
+
|
|
335
|
+
bindIf("S-Down", bindNext, "send-keys S-Down");
|
|
336
|
+
bindIf("S-Up", bindPrev, "send-keys S-Up");
|
|
311
337
|
|
|
312
338
|
// 대체 키: 일부 환경에서 S-Up이 누락될 때 사용
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
);
|
|
316
|
-
tmux(
|
|
317
|
-
`bind-key -T root -n S-Left if-shell -F '${cond}' ${bindPrev} 'send-keys S-Left'`,
|
|
318
|
-
);
|
|
319
|
-
tmux(
|
|
320
|
-
`bind-key -T root -n BTab if-shell -F '${cond}' ${bindPrev} 'send-keys BTab'`,
|
|
321
|
-
);
|
|
339
|
+
bindIf("S-Right", bindNext, "send-keys S-Right");
|
|
340
|
+
bindIf("S-Left", bindPrev, "send-keys S-Left");
|
|
341
|
+
bindIf("BTab", bindPrev, "send-keys BTab");
|
|
322
342
|
|
|
323
343
|
// 현재 활성 pane 인터럽트
|
|
324
|
-
|
|
325
|
-
`bind-key -T root -n Escape if-shell -F '${cond}' 'send-keys C-c' 'send-keys Escape'`,
|
|
326
|
-
);
|
|
344
|
+
bindIf("Escape", "send-keys C-c", "send-keys Escape");
|
|
327
345
|
|
|
328
346
|
// 태스크 목록 토글 (tmux 3.2+ popup 우선, 실패 시 안내 메시지)
|
|
329
347
|
if (taskListCommand) {
|
|
330
348
|
const escaped = taskListCommand.replace(/'/g, "'\\''");
|
|
331
349
|
try {
|
|
332
|
-
|
|
333
|
-
`bind-key -T root -n C-t if-shell -F '${cond}' "display-popup -E '${escaped}'" "send-keys C-t"`,
|
|
334
|
-
);
|
|
350
|
+
bindIf("C-t", `display-popup -E '${escaped}'`, "send-keys C-t");
|
|
335
351
|
} catch {
|
|
336
|
-
|
|
337
|
-
|
|
352
|
+
bindIf(
|
|
353
|
+
"C-t",
|
|
354
|
+
'display-message "tfx multi tasks 명령으로 태스크 확인"',
|
|
355
|
+
"send-keys C-t",
|
|
338
356
|
);
|
|
339
357
|
}
|
|
340
358
|
}
|
|
@@ -432,7 +450,8 @@ export function sessionExists(sessionName) {
|
|
|
432
450
|
}
|
|
433
451
|
|
|
434
452
|
try {
|
|
435
|
-
tmux
|
|
453
|
+
// '=' 가 없으면 tmux 가 접두사로 다른 세션을 잡는다(#548).
|
|
454
|
+
tmux(["has-session", "-t", `=${sessionName}`], { stdio: "ignore" });
|
|
436
455
|
return true;
|
|
437
456
|
} catch {
|
|
438
457
|
return false;
|
|
@@ -450,7 +469,7 @@ export function killSession(sessionName) {
|
|
|
450
469
|
}
|
|
451
470
|
|
|
452
471
|
try {
|
|
453
|
-
tmux(
|
|
472
|
+
tmux(["kill-session", "-t", `=${sessionName}`], { stdio: "ignore" });
|
|
454
473
|
} catch {
|
|
455
474
|
// 이미 종료된 세션 — 무시
|
|
456
475
|
}
|
|
@@ -466,7 +485,7 @@ export function listSessions() {
|
|
|
466
485
|
}
|
|
467
486
|
|
|
468
487
|
try {
|
|
469
|
-
const output = tmux(
|
|
488
|
+
const output = tmux(["list-sessions", "-F", "#{session_name}"]);
|
|
470
489
|
return output.split("\n").filter((s) => s.startsWith("tfx-multi-"));
|
|
471
490
|
} catch {
|
|
472
491
|
return [];
|
|
@@ -484,9 +503,11 @@ export function getSessionAttachedCount(sessionName) {
|
|
|
484
503
|
}
|
|
485
504
|
|
|
486
505
|
try {
|
|
487
|
-
const output = tmux(
|
|
488
|
-
|
|
489
|
-
|
|
506
|
+
const output = tmux([
|
|
507
|
+
"list-sessions",
|
|
508
|
+
"-F",
|
|
509
|
+
"#{session_name} #{session_attached}",
|
|
510
|
+
]);
|
|
490
511
|
const line = output
|
|
491
512
|
.split("\n")
|
|
492
513
|
.find((l) => l.startsWith(`${sessionName} `));
|
|
@@ -511,7 +532,7 @@ export function capturePaneOutput(target, lines = 5) {
|
|
|
511
532
|
|
|
512
533
|
try {
|
|
513
534
|
// -l 플래그는 일부 tmux 빌드(MSYS2)에서 미지원 → 전체 캡처 후 JS에서 절삭
|
|
514
|
-
const full = tmux(
|
|
535
|
+
const full = tmux(["capture-pane", "-t", target, "-p"]);
|
|
515
536
|
const nonEmpty = full.split("\n").filter((l) => l.trim() !== "");
|
|
516
537
|
return nonEmpty.slice(-lines).join("\n");
|
|
517
538
|
} catch {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { platform as osPlatform } from "node:os";
|
|
3
3
|
import { psmuxExec as defaultPsmuxExec } from "./psmux.mjs";
|
|
4
4
|
import { tmuxExec as defaultTmuxExec, detectMultiplexer } from "./session.mjs";
|
|
@@ -17,8 +17,9 @@ export function shellQuote(value) {
|
|
|
17
17
|
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
// PowerShell 은 ‘ ’ ‚ ‛ 도 작은따옴표로 읽는다. 따옴표 문자를 모두 겹쳐 써야 인용이 안 끝난다.
|
|
20
21
|
export function powershellSingleQuote(value) {
|
|
21
|
-
return `'${String(value).replace(/'/g, "
|
|
22
|
+
return `'${String(value).replace(/['\u2018\u2019\u201A\u201B]/g, "$&$&")}'`;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export function buildCommandString({ cwd, command }) {
|
|
@@ -48,14 +49,6 @@ function createTabSpec(spec, title) {
|
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
function execOpenTerminal(execFn) {
|
|
52
|
-
return new Promise((resolve) => {
|
|
53
|
-
execFn("open -a Terminal", { timeout: 5000 }, (error) => {
|
|
54
|
-
resolve(!error);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
|
|
59
52
|
function isTmuxLikeMux(mux) {
|
|
60
53
|
return TMUX_LIKE_MUXES.has(mux);
|
|
61
54
|
}
|
|
@@ -125,7 +118,6 @@ export function createTerminalOpener(deps = {}) {
|
|
|
125
118
|
const platform = resolvePlatform(deps);
|
|
126
119
|
const tmuxExec = deps.tmuxExec || defaultTmuxExec;
|
|
127
120
|
const psmuxExec = deps.psmuxExec || defaultPsmuxExec;
|
|
128
|
-
const exec = deps.exec || defaultExec;
|
|
129
121
|
const createWtManager = deps.createWtManager || defaultCreateWtManager;
|
|
130
122
|
|
|
131
123
|
async function openCommand(spec = {}) {
|
|
@@ -162,10 +154,8 @@ export function createTerminalOpener(deps = {}) {
|
|
|
162
154
|
}
|
|
163
155
|
}
|
|
164
156
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
|
|
157
|
+
// 멀티플렉서가 없으면 명령을 실을 경로가 없다. 빈 Terminal 창을 띄우고
|
|
158
|
+
// 성공이라 하지 않고 실패를 돌려준다(macOS 포함).
|
|
169
159
|
return false;
|
|
170
160
|
}
|
|
171
161
|
|
|
@@ -238,10 +228,6 @@ export function createTerminalOpener(deps = {}) {
|
|
|
238
228
|
return { openCommand, openSession, focusPane };
|
|
239
229
|
}
|
|
240
230
|
|
|
241
|
-
export function openSessionTarget(sessionName, opts = {}) {
|
|
242
|
-
return createTerminalOpener(opts._deps).openSession(sessionName, opts);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
231
|
export function focusSessionPane(sessionName, workerNumber, opts = {}) {
|
|
246
232
|
return createTerminalOpener(opts._deps).focusPane(sessionName, workerNumber);
|
|
247
233
|
}
|
package/hub/team/tui-viewer.mjs
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import { execFileSync } from "node:child_process";
|
|
8
8
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
9
|
-
import { tmpdir } from "node:os";
|
|
10
9
|
import { join } from "node:path";
|
|
11
10
|
import { formatPsmuxInstallGuidance } from "../../scripts/lib/psmux-info.mjs";
|
|
11
|
+
import { privateTmpDir } from "@triflux/core/hub/lib/private-tmp.mjs";
|
|
12
12
|
import { openHeadlessDashboardTarget } from "./dashboard-open.mjs";
|
|
13
13
|
import { processHandoff } from "./handoff.mjs";
|
|
14
14
|
import { getMultiplexerType } from "./psmux.mjs";
|
|
@@ -23,7 +23,7 @@ function argVal(flag) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const SESSION = argVal("--session");
|
|
26
|
-
const RESULT_DIR = argVal("--result-dir") ??
|
|
26
|
+
const RESULT_DIR = argVal("--result-dir") ?? privateTmpDir("tfx-headless");
|
|
27
27
|
const LAYOUT = argVal("--layout") ?? "single";
|
|
28
28
|
const LEAD_PANE = argVal("--lead-pane");
|
|
29
29
|
const MUX_BIN = getMultiplexerType();
|
package/hub/team/tui.mjs
CHANGED
|
@@ -290,8 +290,6 @@ const SUMMARY_KEYS = [
|
|
|
290
290
|
"partial_output",
|
|
291
291
|
];
|
|
292
292
|
|
|
293
|
-
// 텍스트/상태/색상 유틸은 tui-core.mjs에서 import (위 참조)
|
|
294
|
-
|
|
295
293
|
// ── MOCHA RGB (gradual fade 보간용) ──
|
|
296
294
|
const MOCHA_RGB = {
|
|
297
295
|
ok: { r: 166, g: 227, b: 161 },
|
|
@@ -380,8 +378,6 @@ function dedupeRole(role, name, cli) {
|
|
|
380
378
|
return r;
|
|
381
379
|
}
|
|
382
380
|
|
|
383
|
-
// wrapLine, wrapTextAll → tui-core.mjs에서 import
|
|
384
|
-
|
|
385
381
|
// ── virtual row buffer ────────────────────────────────────────────────────
|
|
386
382
|
class RowBuffer {
|
|
387
383
|
constructor() {
|
|
@@ -415,8 +411,6 @@ class RowBuffer {
|
|
|
415
411
|
}
|
|
416
412
|
}
|
|
417
413
|
|
|
418
|
-
// countStatuses → tui-core.mjs에서 import
|
|
419
|
-
|
|
420
414
|
// ── Tier1: 상단 고정 1행 ─────────────────────────────────────────────────
|
|
421
415
|
function phaseColor(phase, time = Date.now()) {
|
|
422
416
|
const shimmer = currentShimmer(time);
|
package/hub/workers/factory.mjs
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
// hub/workers/factory.mjs — Worker 생성 팩토리
|
|
2
|
-
//
|
|
3
|
-
// Supported worker types:
|
|
4
|
-
// - 'claude' → ClaudeWorker
|
|
5
|
-
// - 'codex' → CodexMcpWorker
|
|
6
|
-
//
|
|
7
2
|
import { ClaudeWorker } from "./claude-worker.mjs";
|
|
8
3
|
|
|
9
4
|
/**
|
|
10
|
-
* @param {'claude'
|
|
5
|
+
* @param {'claude'} type
|
|
11
6
|
* @param {object} [opts]
|
|
12
7
|
* @returns {Promise<import('./interface.mjs').IWorker>}
|
|
13
8
|
*/
|
|
@@ -15,10 +10,6 @@ export async function createWorker(type, opts = {}) {
|
|
|
15
10
|
switch (type) {
|
|
16
11
|
case "claude":
|
|
17
12
|
return new ClaudeWorker(opts);
|
|
18
|
-
case "codex": {
|
|
19
|
-
const { CodexMcpWorker } = await import("./codex-mcp.mjs");
|
|
20
|
-
return new CodexMcpWorker(opts);
|
|
21
|
-
}
|
|
22
13
|
default:
|
|
23
14
|
throw new Error(`Unknown worker type: ${type}`);
|
|
24
15
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@triflux/remote",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.55.0",
|
|
4
4
|
"description": "triflux remote — team mode, psmux, MCP workers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "hub/index.mjs",
|
|
@@ -9,17 +9,13 @@
|
|
|
9
9
|
"./hub/*": "./hub/*"
|
|
10
10
|
},
|
|
11
11
|
"engines": {
|
|
12
|
-
"node": ">=
|
|
12
|
+
"node": ">=22"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
15
|
"@triflux/core": "^10.0.0-alpha.1"
|
|
16
16
|
},
|
|
17
|
-
"dependencies": {
|
|
18
|
-
"@modelcontextprotocol/sdk": "^1.27.1"
|
|
19
|
-
},
|
|
20
17
|
"files": [
|
|
21
18
|
"hub",
|
|
22
|
-
"cto",
|
|
23
19
|
"scripts/lib"
|
|
24
20
|
],
|
|
25
21
|
"keywords": [
|