triflux 8.0.0 → 8.2.2
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 +40 -1
- package/hub/lib/process-utils.mjs +123 -0
- package/hub/server.mjs +48 -1
- package/hub/team/ansi.mjs +161 -19
- package/hub/team/backend.mjs +1 -1
- package/hub/team/cli/commands/start/index.mjs +3 -2
- package/hub/team/cli/commands/start/parse-args.mjs +9 -0
- package/hub/team/cli/commands/start/start-headless.mjs +6 -3
- package/hub/team/cli/help.mjs +2 -0
- package/hub/team/dashboard-layout.mjs +31 -0
- package/hub/team/headless.mjs +146 -33
- package/hub/team/psmux.mjs +174 -7
- package/hub/team/tui-viewer.mjs +354 -90
- package/hub/team/tui.mjs +856 -67
- package/package.json +1 -1
- package/scripts/remote-spawn.mjs +92 -12
- package/scripts/tfx-route.sh +17 -8
package/package.json
CHANGED
package/scripts/remote-spawn.mjs
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
// node remote-spawn.mjs --probe <ssh-host>
|
|
11
11
|
|
|
12
12
|
import { randomUUID } from "crypto";
|
|
13
|
-
import { execFileSync, spawn } from "child_process";
|
|
14
|
-
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
13
|
+
import { execFileSync, execSync, spawn } from "child_process";
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
15
15
|
import { homedir, platform as getPlatform, tmpdir } from "os";
|
|
16
16
|
import { join, posix as posixPath, resolve, win32 as win32Path } from "path";
|
|
17
17
|
import {
|
|
@@ -184,24 +184,76 @@ function parseArgs(argv) {
|
|
|
184
184
|
};
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
function parseVersion(versionStr) {
|
|
188
|
+
const match = /(\d+)\.(\d+)\.(\d+)/.exec(versionStr);
|
|
189
|
+
if (!match) return null;
|
|
190
|
+
return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function compareVersions(a, b) {
|
|
194
|
+
for (let i = 0; i < 3; i++) {
|
|
195
|
+
if (a[i] !== b[i]) return a[i] - b[i];
|
|
196
|
+
}
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function probeVersion(binPath) {
|
|
201
|
+
try {
|
|
202
|
+
if (/\.(cmd|bat)$/iu.test(binPath)) {
|
|
203
|
+
// .cmd/.bat → execSync로 shell 경유 (execFileSync EINVAL 회피)
|
|
204
|
+
const out = execSync(`"${binPath}" --version`, {
|
|
205
|
+
encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"],
|
|
206
|
+
});
|
|
207
|
+
return parseVersion(out);
|
|
208
|
+
}
|
|
209
|
+
const out = execFileSync(binPath, ["--version"], {
|
|
210
|
+
encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"],
|
|
211
|
+
});
|
|
212
|
+
return parseVersion(out);
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
187
218
|
function detectClaudePath() {
|
|
188
219
|
if (process.env.CLAUDE_BIN_PATH) return process.env.CLAUDE_BIN_PATH;
|
|
189
220
|
|
|
221
|
+
const candidates = [];
|
|
222
|
+
|
|
190
223
|
const wingetPath = join(homedir(), "AppData", "Local", "Microsoft", "WinGet", "Links", "claude.exe");
|
|
191
|
-
if (existsSync(wingetPath))
|
|
224
|
+
if (existsSync(wingetPath)) candidates.push(wingetPath);
|
|
192
225
|
|
|
193
226
|
const npmPath = join(process.env.APPDATA || "", "npm", "claude.cmd");
|
|
194
|
-
if (existsSync(npmPath))
|
|
227
|
+
if (existsSync(npmPath)) candidates.push(npmPath);
|
|
195
228
|
|
|
196
229
|
try {
|
|
197
230
|
const command = IS_WINDOWS_LOCAL ? "where" : "which";
|
|
198
231
|
const result = execFileSync(command, ["claude"], { encoding: "utf8", timeout: 5000 }).trim();
|
|
199
|
-
if (result)
|
|
232
|
+
if (result) {
|
|
233
|
+
for (const line of result.split(/\r?\n/u)) {
|
|
234
|
+
const p = line.trim();
|
|
235
|
+
if (p && !candidates.includes(p)) candidates.push(p);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
200
238
|
} catch {
|
|
201
239
|
// not found
|
|
202
240
|
}
|
|
203
241
|
|
|
204
|
-
return "claude";
|
|
242
|
+
if (candidates.length === 0) return "claude";
|
|
243
|
+
|
|
244
|
+
let bestPath = candidates[0];
|
|
245
|
+
let bestVersion = probeVersion(candidates[0]);
|
|
246
|
+
|
|
247
|
+
for (const candidate of candidates.slice(1)) {
|
|
248
|
+
const ver = probeVersion(candidate);
|
|
249
|
+
if (ver === null) continue;
|
|
250
|
+
if (bestVersion === null || compareVersions(ver, bestVersion) > 0) {
|
|
251
|
+
bestVersion = ver;
|
|
252
|
+
bestPath = candidate;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return bestPath;
|
|
205
257
|
}
|
|
206
258
|
|
|
207
259
|
function getPermissionFlag() {
|
|
@@ -598,16 +650,44 @@ function spawnLocal(args, claudePath, prompt) {
|
|
|
598
650
|
const sessionName = `tfx-spawn-${randomUUID().slice(0, 8)}`;
|
|
599
651
|
const paneId = `${sessionName}:0.0`;
|
|
600
652
|
const permissionFlags = getPermissionFlag().join(" ");
|
|
601
|
-
const
|
|
653
|
+
const claudePathNorm = normalizeCommandPath(claudePath);
|
|
654
|
+
|
|
655
|
+
// 임시파일 생성 (프롬프트가 있을 때만)
|
|
656
|
+
// 정리는 pwsh 스크립트 내부에서 수행 (Node exit 시 삭제하면 pane 실행 전 사라짐)
|
|
657
|
+
let tmpFile = null;
|
|
658
|
+
if (prompt) {
|
|
659
|
+
tmpFile = join(tmpdir(), `tfx-prompt-${randomUUID().slice(0, 8)}.md`);
|
|
660
|
+
writeFileSync(tmpFile, prompt, { encoding: "utf8" });
|
|
661
|
+
}
|
|
602
662
|
|
|
603
663
|
createPsmuxSession(sessionName, { layout: "1xN", paneCount: 1 });
|
|
604
664
|
sendKeysToPane(paneId, `cd '${escapePwshSingleQuoted(dir)}'`);
|
|
605
|
-
sleepMs(
|
|
606
|
-
|
|
607
|
-
if (prompt) {
|
|
608
|
-
|
|
609
|
-
|
|
665
|
+
sleepMs(300);
|
|
666
|
+
|
|
667
|
+
if (prompt && tmpFile) {
|
|
668
|
+
// pwsh -File 패턴: 인라인 쿼팅 문제 회피 (피드백: -Command 금지)
|
|
669
|
+
// 1단계: 프롬프트를 Get-Content -Raw → claude -p (one-shot), 세션 ID 추출
|
|
670
|
+
// 2단계: --resume으로 인터랙티브 세션 이어붙이기
|
|
671
|
+
const tmpFileNorm = normalizeCommandPath(tmpFile);
|
|
672
|
+
const flags = getPermissionFlag().map((f) => `'${escapePwshSingleQuoted(f)}'`).join(", ");
|
|
673
|
+
const scriptContent = [
|
|
674
|
+
`$ErrorActionPreference = 'SilentlyContinue'`,
|
|
675
|
+
`$t = '${escapePwshSingleQuoted(tmpFileNorm)}'`,
|
|
676
|
+
`$c = '${escapePwshSingleQuoted(claudePathNorm)}'`,
|
|
677
|
+
`$f = @(${flags})`,
|
|
678
|
+
`$raw = Get-Content -Raw $t`,
|
|
679
|
+
`Remove-Item -ErrorAction SilentlyContinue $t`,
|
|
680
|
+
`Remove-Item -ErrorAction SilentlyContinue $MyInvocation.MyCommand.Definition`,
|
|
681
|
+
`& $c @f $raw`,
|
|
682
|
+
].join("\n");
|
|
683
|
+
const scriptFile = join(tmpdir(), `tfx-spawn-${randomUUID().slice(0, 8)}.ps1`);
|
|
684
|
+
writeFileSync(scriptFile, scriptContent, { encoding: "utf8" });
|
|
685
|
+
sendKeysToPane(paneId, `pwsh -NoProfile -NoExit -File '${escapePwshSingleQuoted(normalizeCommandPath(scriptFile))}'`);
|
|
686
|
+
} else {
|
|
687
|
+
const command = `& '${escapePwshSingleQuoted(claudePathNorm)}'${permissionFlags ? ` ${permissionFlags}` : ""}`;
|
|
688
|
+
sendKeysToPane(paneId, command);
|
|
610
689
|
}
|
|
690
|
+
|
|
611
691
|
openAttachTab(sessionName, "Claude@local");
|
|
612
692
|
console.log(sessionName);
|
|
613
693
|
}
|
package/scripts/tfx-route.sh
CHANGED
|
@@ -29,6 +29,15 @@ VERSION="2.5"
|
|
|
29
29
|
|
|
30
30
|
set -euo pipefail
|
|
31
31
|
|
|
32
|
+
# ── timeout 명령 호환성 — Windows에서 TIMEOUT.exe 대신 Git Bash coreutils timeout 사용 ──
|
|
33
|
+
if command -v /usr/bin/timeout >/dev/null 2>&1; then
|
|
34
|
+
TIMEOUT_BIN="/usr/bin/timeout"
|
|
35
|
+
elif command -v gtimeout >/dev/null 2>&1; then
|
|
36
|
+
TIMEOUT_BIN="gtimeout" # macOS homebrew
|
|
37
|
+
else
|
|
38
|
+
TIMEOUT_BIN="timeout" # Linux 기본
|
|
39
|
+
fi
|
|
40
|
+
|
|
32
41
|
# ── Async Job 디렉토리 ──
|
|
33
42
|
TFX_JOBS_DIR="${TMPDIR:-/tmp}/tfx-jobs"
|
|
34
43
|
|
|
@@ -1120,9 +1129,9 @@ run_stream_worker() {
|
|
|
1120
1129
|
)
|
|
1121
1130
|
|
|
1122
1131
|
if [[ "$use_tee_flag" == "true" ]]; then
|
|
1123
|
-
printf '%s' "$prompt" |
|
|
1132
|
+
printf '%s' "$prompt" | "$TIMEOUT_BIN" "$TIMEOUT_SEC" "${worker_cmd[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
|
|
1124
1133
|
else
|
|
1125
|
-
printf '%s' "$prompt" |
|
|
1134
|
+
printf '%s' "$prompt" | "$TIMEOUT_BIN" "$TIMEOUT_SEC" "${worker_cmd[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
|
|
1126
1135
|
fi
|
|
1127
1136
|
worker_pid=$!
|
|
1128
1137
|
|
|
@@ -1144,9 +1153,9 @@ _gemini_run_once() {
|
|
|
1144
1153
|
local -a g_args=("$@")
|
|
1145
1154
|
|
|
1146
1155
|
if [[ "$use_tee_flag" == "true" ]]; then
|
|
1147
|
-
|
|
1156
|
+
"$TIMEOUT_BIN" "$TIMEOUT_SEC" "$CLI_CMD" "${g_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
|
|
1148
1157
|
else
|
|
1149
|
-
|
|
1158
|
+
"$TIMEOUT_BIN" "$TIMEOUT_SEC" "$CLI_CMD" "${g_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
|
|
1150
1159
|
fi
|
|
1151
1160
|
echo "$!"
|
|
1152
1161
|
}
|
|
@@ -1285,9 +1294,9 @@ run_codex_exec() {
|
|
|
1285
1294
|
fi
|
|
1286
1295
|
|
|
1287
1296
|
if [[ "$use_tee_flag" == "true" ]]; then
|
|
1288
|
-
|
|
1297
|
+
"$TIMEOUT_BIN" "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
|
|
1289
1298
|
else
|
|
1290
|
-
|
|
1299
|
+
"$TIMEOUT_BIN" "$TIMEOUT_SEC" "$CLI_CMD" "${codex_args[@]}" "$prompt" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
|
|
1291
1300
|
fi
|
|
1292
1301
|
worker_pid=$!
|
|
1293
1302
|
|
|
@@ -1379,9 +1388,9 @@ run_codex_mcp() {
|
|
|
1379
1388
|
esac
|
|
1380
1389
|
|
|
1381
1390
|
if [[ "$use_tee_flag" == "true" ]]; then
|
|
1382
|
-
|
|
1391
|
+
"$TIMEOUT_BIN" "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" 2>"$STDERR_LOG" | tee "$STDOUT_LOG" &
|
|
1383
1392
|
else
|
|
1384
|
-
|
|
1393
|
+
"$TIMEOUT_BIN" "$TIMEOUT_SEC" "$node_bin" "${mcp_args[@]}" >"$STDOUT_LOG" 2>"$STDERR_LOG" &
|
|
1385
1394
|
fi
|
|
1386
1395
|
worker_pid=$!
|
|
1387
1396
|
|