triflux 6.0.20 → 6.0.21
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.
|
@@ -38,7 +38,7 @@ function renderTmuxInstallHelp() {
|
|
|
38
38
|
export { parseTeamArgs };
|
|
39
39
|
|
|
40
40
|
export async function teamStart(args = []) {
|
|
41
|
-
const { agents, lead, layout, teammateMode, task: rawTask, assigns, autoAttach, progressive, timeoutSec } = parseTeamArgs(args);
|
|
41
|
+
const { agents, lead, layout, teammateMode, task: rawTask, assigns, autoAttach, progressive, timeoutSec, verbose } = parseTeamArgs(args);
|
|
42
42
|
// --assign 사용 시 task를 자동 생성
|
|
43
43
|
const task = rawTask || (assigns.length > 0 ? assigns.map(a => a.prompt).join(" + ") : "");
|
|
44
44
|
if (!task) return printStartUsage();
|
|
@@ -72,7 +72,7 @@ export async function teamStart(args = []) {
|
|
|
72
72
|
const state = effectiveMode === "in-process"
|
|
73
73
|
? await startInProcessTeam({ sessionId, task, lead, agents, subtasks, hubUrl })
|
|
74
74
|
: effectiveMode === "headless"
|
|
75
|
-
? await startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec })
|
|
75
|
+
? await startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec, verbose })
|
|
76
76
|
: effectiveMode === "wt"
|
|
77
77
|
? await startWtTeam({ sessionId, task, lead, agents, subtasks, layout, hubUrl })
|
|
78
78
|
: await startMuxTeam({ sessionId, task, lead, agents, subtasks, layout, hubUrl, teammateMode: effectiveMode });
|
|
@@ -7,9 +7,10 @@ export function parseTeamArgs(args = []) {
|
|
|
7
7
|
let teammateMode = "auto";
|
|
8
8
|
const taskParts = [];
|
|
9
9
|
const assigns = []; // --assign "codex:프롬프트:역할" 형식
|
|
10
|
-
let autoAttach =
|
|
10
|
+
let autoAttach = false;
|
|
11
11
|
let progressive = true;
|
|
12
12
|
let timeoutSec = 300;
|
|
13
|
+
let verbose = false;
|
|
13
14
|
|
|
14
15
|
for (let index = 0; index < args.length; index += 1) {
|
|
15
16
|
const current = args[index];
|
|
@@ -29,6 +30,10 @@ export function parseTeamArgs(args = []) {
|
|
|
29
30
|
}
|
|
30
31
|
} else if (current === "--auto-attach") {
|
|
31
32
|
autoAttach = true;
|
|
33
|
+
} else if (current === "--no-auto-attach") {
|
|
34
|
+
autoAttach = false;
|
|
35
|
+
} else if (current === "--verbose") {
|
|
36
|
+
verbose = true;
|
|
32
37
|
} else if (current === "--no-progressive") {
|
|
33
38
|
progressive = false;
|
|
34
39
|
} else if (current === "--timeout" && args[index + 1]) {
|
|
@@ -48,5 +53,6 @@ export function parseTeamArgs(args = []) {
|
|
|
48
53
|
autoAttach,
|
|
49
54
|
progressive,
|
|
50
55
|
timeoutSec,
|
|
56
|
+
verbose,
|
|
51
57
|
};
|
|
52
58
|
}
|
|
@@ -4,23 +4,21 @@ import { ok, warn } from "../../render.mjs";
|
|
|
4
4
|
import { buildTasks } from "../../services/task-model.mjs";
|
|
5
5
|
import { clearTeamState } from "../../services/state-store.mjs";
|
|
6
6
|
|
|
7
|
-
export async function startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec }) {
|
|
8
|
-
console.log(` ${AMBER}모드: headless (Lead-Direct v6.0.0)${RESET}`);
|
|
9
|
-
|
|
7
|
+
export async function startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec, verbose }) {
|
|
10
8
|
// --assign이 있으면 그것을 사용, 없으면 agents+subtasks 조합
|
|
11
9
|
const assignments = assigns && assigns.length > 0
|
|
12
10
|
? assigns.map((a, i) => ({ cli: a.cli, prompt: a.prompt, role: a.role || `worker-${i + 1}` }))
|
|
13
11
|
: subtasks.map((subtask, i) => ({ cli: agents[i] || agents[0], prompt: subtask, role: `worker-${i + 1}` }));
|
|
14
12
|
|
|
15
|
-
ok(
|
|
13
|
+
ok(`headless ${assignments.length}워커 시작`);
|
|
16
14
|
|
|
17
15
|
const handle = await runHeadlessInteractive(sessionId, assignments, {
|
|
18
16
|
timeoutSec: timeoutSec || 300,
|
|
19
17
|
layout,
|
|
20
|
-
autoAttach: autoAttach
|
|
21
|
-
progressive: progressive !== false,
|
|
22
|
-
progressIntervalSec: 10,
|
|
23
|
-
onProgress(event) {
|
|
18
|
+
autoAttach: !!autoAttach,
|
|
19
|
+
progressive: progressive !== false,
|
|
20
|
+
progressIntervalSec: verbose ? 10 : 0,
|
|
21
|
+
onProgress: verbose ? function onProgress(event) {
|
|
24
22
|
if (event.type === "session_created") {
|
|
25
23
|
console.log(` ${DIM}세션: ${event.sessionName}${RESET}`);
|
|
26
24
|
} else if (event.type === "worker_added") {
|
|
@@ -34,28 +32,36 @@ export async function startHeadlessTeam({ sessionId, task, lead, agents, subtask
|
|
|
34
32
|
const icon = event.matched && event.exitCode === 0 ? `${GREEN}✓${RESET}` : `${AMBER}✗${RESET}`;
|
|
35
33
|
console.log(` ${icon} [${event.paneName}] ${event.cli} exit=${event.exitCode}${event.sessionDead ? " (dead)" : ""}`);
|
|
36
34
|
}
|
|
37
|
-
},
|
|
35
|
+
} : undefined,
|
|
38
36
|
});
|
|
39
37
|
|
|
40
|
-
// 결과 요약
|
|
38
|
+
// 최소 결과 요약
|
|
41
39
|
const results = handle.results;
|
|
42
40
|
const succeeded = results.filter((r) => r.matched && r.exitCode === 0);
|
|
43
41
|
const failed = results.filter((r) => !r.matched || r.exitCode !== 0);
|
|
44
42
|
|
|
45
|
-
|
|
46
|
-
console.log(` ${DIM}성공: ${succeeded.length} / 실패: ${failed.length} / 전체: ${results.length}${RESET}`);
|
|
43
|
+
ok(`헤드리스 완료: ${succeeded.length}성공 / ${failed.length}실패 / ${results.length}전체`);
|
|
47
44
|
|
|
48
45
|
if (failed.length > 0) {
|
|
49
|
-
|
|
50
|
-
for (const r of failed) console.log(` ${r.paneName} (${r.cli}): exit=${r.exitCode}`);
|
|
46
|
+
for (const r of failed) console.log(` ${AMBER}✗${RESET} ${r.paneName} (${r.cli}) exit=${r.exitCode}`);
|
|
51
47
|
}
|
|
52
48
|
|
|
53
|
-
// 결과
|
|
49
|
+
// 결과 파일 경로 (Lead가 필요시 Read()로 확인)
|
|
54
50
|
for (const r of results) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
console.log(
|
|
58
|
-
|
|
51
|
+
const icon = r.matched && r.exitCode === 0 ? `${GREEN}✓${RESET}` : `${AMBER}✗${RESET}`;
|
|
52
|
+
if (r.resultFile) {
|
|
53
|
+
console.log(` ${icon} ${r.paneName}: ${r.resultFile}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --verbose: 기존 장황한 출력 (200자 preview)
|
|
58
|
+
if (verbose) {
|
|
59
|
+
for (const r of results) {
|
|
60
|
+
if (r.output) {
|
|
61
|
+
const preview = r.output.length > 200 ? `${r.output.slice(0, 200)}…` : r.output;
|
|
62
|
+
console.log(`\n ${DIM}── ${r.paneName} (${r.cli}${r.role ? `, ${r.role}` : ""}) ──${RESET}`);
|
|
63
|
+
console.log(` ${preview}`);
|
|
64
|
+
}
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
67
|
|
package/hub/team/headless.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
|
-
import { execSync, execFileSync } from "node:child_process";
|
|
9
|
+
import { execSync, execFileSync, spawn } from "node:child_process";
|
|
10
10
|
import {
|
|
11
11
|
createPsmuxSession,
|
|
12
12
|
killPsmuxSession,
|
|
@@ -39,14 +39,16 @@ const ANSI_DIM = "\x1b[2m";
|
|
|
39
39
|
export function buildHeadlessCommand(cli, prompt, resultFile) {
|
|
40
40
|
// 프롬프트의 단일 인용부호를 이스케이프
|
|
41
41
|
const escaped = prompt.replace(/'/g, "''");
|
|
42
|
+
// Clear-Host: 실행 즉시 이전 PS 프롬프트 + 명령 텍스트를 깨끗이 지움
|
|
43
|
+
const cls = "Clear-Host; ";
|
|
42
44
|
|
|
43
45
|
switch (cli) {
|
|
44
46
|
case "codex":
|
|
45
|
-
return
|
|
47
|
+
return `${cls}codex exec '${escaped}' -o '${resultFile}' --color never`;
|
|
46
48
|
case "gemini":
|
|
47
|
-
return
|
|
49
|
+
return `${cls}gemini -p '${escaped}' -o text > '${resultFile}' 2>'${resultFile}.err'`;
|
|
48
50
|
case "claude":
|
|
49
|
-
return
|
|
51
|
+
return `${cls}claude -p '${escaped}' --output-format text > '${resultFile}' 2>&1`;
|
|
50
52
|
default:
|
|
51
53
|
throw new Error(`지원하지 않는 CLI: ${cli}`);
|
|
52
54
|
}
|
|
@@ -156,7 +158,8 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
156
158
|
const paneName = `worker-${i + 1}`;
|
|
157
159
|
const resultFile = join(RESULT_DIR, `${sessionName}-${paneName}.txt`).replace(/\\/g, "/");
|
|
158
160
|
const cmd = buildHeadlessCommand(assignment.cli, assignment.prompt, resultFile);
|
|
159
|
-
const
|
|
161
|
+
const scriptDir = join(RESULT_DIR, sessionName);
|
|
162
|
+
const dispatch = dispatchCommand(sessionName, paneName, cmd, { scriptDir, scriptName: paneName });
|
|
160
163
|
|
|
161
164
|
// P1 fix: 비-progressive에서는 pane 리네임 금지 — 캡처 로그 경로가 타이틀 기반이므로
|
|
162
165
|
// 리네임하면 waitForCompletion이 "codex (role).log"를 찾지만 실제는 "worker-N.log"로 불일치
|
|
@@ -216,6 +219,7 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
216
219
|
matched: completion.matched,
|
|
217
220
|
exitCode: completion.exitCode,
|
|
218
221
|
output,
|
|
222
|
+
resultFile: d.resultFile,
|
|
219
223
|
sessionDead: completion.sessionDead || false,
|
|
220
224
|
};
|
|
221
225
|
}));
|
|
@@ -278,6 +282,7 @@ export function applyTrifluxTheme(sessionName) {
|
|
|
278
282
|
* 반투명 + 비포커스 시 더 투명 + Catppuccin 테마.
|
|
279
283
|
* @returns {boolean} 성공 여부
|
|
280
284
|
*/
|
|
285
|
+
|
|
281
286
|
/**
|
|
282
287
|
* WT 기본 프로필의 폰트 크기를 읽는다.
|
|
283
288
|
* @returns {number} 기본 폰트 크기 (못 읽으면 12)
|
|
@@ -357,52 +362,72 @@ export function ensureWtProfile(workerCount = 2) {
|
|
|
357
362
|
*/
|
|
358
363
|
export function autoAttachTerminal(sessionName, opts = {}, workerCount = 2) {
|
|
359
364
|
try {
|
|
360
|
-
// Windows Terminal이 설치되어 있는지 확인
|
|
361
365
|
execSync("where wt.exe", { stdio: "ignore" });
|
|
362
366
|
} catch {
|
|
363
|
-
return false;
|
|
367
|
+
return false;
|
|
364
368
|
}
|
|
365
369
|
|
|
366
|
-
// triflux WT 프로필 확보 (투명도 + 테마 + 폰트 크기)
|
|
367
370
|
ensureWtProfile(workerCount);
|
|
368
371
|
|
|
369
|
-
const
|
|
370
|
-
const mode = opts.mode || "auto"; // "split" | "window" | "auto"
|
|
371
|
-
|
|
372
|
-
// v6.0.20: 기본 popup(별도 창). split은 --split 명시 시에만.
|
|
373
|
-
// 이유: WT sp는 포커스된 pane을 분할하므로, 사용자가 다른 탭에 있으면
|
|
374
|
-
// 엉뚱한 곳이 분할됨. 별도 창은 포커스 무관하게 안정적.
|
|
375
|
-
const preferSplit = mode === "split";
|
|
376
|
-
const preferWindow = mode !== "split";
|
|
372
|
+
const mode = opts.mode || "auto";
|
|
377
373
|
|
|
378
|
-
if (
|
|
379
|
-
// inner split — 같은 WT 창에서 가로
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
}
|
|
374
|
+
if (mode === "split" && process.env.WT_SESSION) {
|
|
375
|
+
// inner split — 같은 WT 창에서 가로 분할. psmux를 직접 실행 (pwsh 불필요).
|
|
376
|
+
try {
|
|
377
|
+
const child = spawn("wt.exe", [
|
|
378
|
+
"-w", "0", "sp", "-H", "-s", "0.50",
|
|
379
|
+
"--profile", "triflux", "--title", "triflux",
|
|
380
|
+
"--", "psmux", "attach", "-t", sessionName,
|
|
381
|
+
], { detached: true, stdio: "ignore" });
|
|
382
|
+
child.unref();
|
|
383
|
+
try { spawn("wt.exe", ["-w", "0", "mf", "up"], { detached: true, stdio: "ignore" }).unref(); } catch { /* 무시 */ }
|
|
384
|
+
return true;
|
|
385
|
+
} catch { /* fallthrough to window */ }
|
|
391
386
|
}
|
|
392
|
-
|
|
387
|
+
|
|
388
|
+
// v6.0.22: 읽기전용 뷰어 + 최소화 트릭.
|
|
389
|
+
// 1. 현재 HWND 캡처 → 2. WT 열기(읽기전용 뷰어) → 3. 새 창 즉시 최소화
|
|
390
|
+
// → Windows가 자동으로 이전 창에 포커스 복원. 사용자는 작업표시줄에서 확인.
|
|
391
|
+
const logDir = join(tmpdir(), "psmux-steering", sessionName).replace(/\\/g, "/");
|
|
393
392
|
const cols = Math.max(100, 60 + workerCount * 15);
|
|
394
393
|
const rows = Math.max(25, 15 + workerCount * 4);
|
|
395
|
-
|
|
396
|
-
|
|
394
|
+
|
|
395
|
+
// 읽기전용 뷰어 스크립트 생성
|
|
396
|
+
const viewerScript = join(tmpdir(), "tfx-viewer-" + sessionName + ".ps1").replace(/\\/g, "/");
|
|
397
|
+
writeFileSync(viewerScript, [
|
|
398
|
+
"$Host.UI.RawUI.WindowTitle = '" + sessionName + "'",
|
|
399
|
+
"Write-Host \"`e[38;5;214m⬡ triflux viewer (read-only)`e[0m — " + sessionName + "\"",
|
|
400
|
+
"Write-Host 'Log: " + logDir + "'",
|
|
401
|
+
"Write-Host '---'",
|
|
402
|
+
"for ($i=0; $i -lt 10; $i++) { if (Test-Path '" + logDir + "') { break }; Start-Sleep 1 }",
|
|
403
|
+
"Get-Content -Path '" + logDir + "\\*.log' -Wait -Tail 50",
|
|
404
|
+
].join("\n"), "utf8");
|
|
405
|
+
|
|
406
|
+
// v6.0.23: 같은 WT 창에 탭 추가 (-w 0).
|
|
407
|
+
// WT에 "이전 탭" CLI 명령이 없으므로, 탭 추가 후 Ctrl+Shift+Tab 시뮬레이션.
|
|
408
|
+
try {
|
|
409
|
+
const child = spawn("wt.exe", [
|
|
410
|
+
"-w", "0", "nt",
|
|
411
|
+
"--profile", "triflux",
|
|
412
|
+
"--title", sessionName,
|
|
413
|
+
"--", "pwsh.exe", "-NoProfile", "-NoLogo", "-File", viewerScript,
|
|
414
|
+
], { detached: true, stdio: "ignore" });
|
|
415
|
+
child.unref();
|
|
416
|
+
} catch {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 300ms 후 Ctrl+Shift+Tab → "이전 탭"으로 복귀 (탭 인덱스 무관)
|
|
421
|
+
const prevTabScript = "Start-Sleep -Milliseconds 300; Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('^+{TAB}')";
|
|
422
|
+
for (const shell of ["pwsh.exe", "powershell.exe"]) {
|
|
397
423
|
try {
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
return true;
|
|
424
|
+
spawn(shell, ["-NoProfile", "-NonInteractive", "-Command", prevTabScript], {
|
|
425
|
+
detached: true, stdio: "ignore",
|
|
426
|
+
}).unref();
|
|
427
|
+
break;
|
|
403
428
|
} catch { /* 다음 shell */ }
|
|
404
429
|
}
|
|
405
|
-
return
|
|
430
|
+
return true;
|
|
406
431
|
}
|
|
407
432
|
|
|
408
433
|
/**
|
package/hub/team/psmux.mjs
CHANGED
|
@@ -630,6 +630,7 @@ export function dispatchCommand(sessionName, paneNameOrTarget, commandText) {
|
|
|
630
630
|
|
|
631
631
|
const token = randomToken(paneName);
|
|
632
632
|
const wrapped = `${commandText}; $trifluxExit = if ($null -ne $LASTEXITCODE) { [int]$LASTEXITCODE } else { 0 }; Write-Output "${COMPLETION_PREFIX}${token}:$trifluxExit"`;
|
|
633
|
+
|
|
633
634
|
sendLiteralToPane(pane.paneId, wrapped, true);
|
|
634
635
|
|
|
635
636
|
return { paneId: pane.paneId, paneName, token, logPath };
|