triflux 6.1.3 → 7.0.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.
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// hub/team/ansi.mjs — Zero-dependency ANSI escape 유틸리티
|
|
2
|
+
// TUI 대시보드 렌더링을 위한 최소 헬퍼.
|
|
3
|
+
|
|
4
|
+
export const ESC = "\x1b";
|
|
5
|
+
|
|
6
|
+
// ── 화면 ──
|
|
7
|
+
export const altScreenOn = `${ESC}[?1049h`;
|
|
8
|
+
export const altScreenOff = `${ESC}[?1049l`;
|
|
9
|
+
export const clearScreen = `${ESC}[2J`;
|
|
10
|
+
export const cursorHome = `${ESC}[H`;
|
|
11
|
+
export const cursorHide = `${ESC}[?25l`;
|
|
12
|
+
export const cursorShow = `${ESC}[?25h`;
|
|
13
|
+
|
|
14
|
+
// ── 커서 이동 ──
|
|
15
|
+
export function moveTo(row, col) { return `${ESC}[${row};${col}H`; }
|
|
16
|
+
export function moveUp(n = 1) { return `${ESC}[${n}A`; }
|
|
17
|
+
export function moveDown(n = 1) { return `${ESC}[${n}B`; }
|
|
18
|
+
|
|
19
|
+
// ── 줄 제어 ──
|
|
20
|
+
export const clearLine = `${ESC}[2K`;
|
|
21
|
+
export const clearToEnd = `${ESC}[K`;
|
|
22
|
+
|
|
23
|
+
// ── 색상 (triflux 디자인 시스템) ──
|
|
24
|
+
export const RESET = `${ESC}[0m`;
|
|
25
|
+
export const BOLD = `${ESC}[1m`;
|
|
26
|
+
export const DIM = `${ESC}[2m`;
|
|
27
|
+
|
|
28
|
+
export const FG = {
|
|
29
|
+
white: `${ESC}[97m`,
|
|
30
|
+
black: `${ESC}[30m`,
|
|
31
|
+
red: `${ESC}[31m`,
|
|
32
|
+
green: `${ESC}[32m`,
|
|
33
|
+
yellow: `${ESC}[33m`,
|
|
34
|
+
blue: `${ESC}[34m`,
|
|
35
|
+
magenta: `${ESC}[35m`,
|
|
36
|
+
cyan: `${ESC}[36m`,
|
|
37
|
+
gray: `${ESC}[90m`,
|
|
38
|
+
// triflux 브랜드
|
|
39
|
+
codex: `${ESC}[97m`, // bright white
|
|
40
|
+
gemini: `${ESC}[38;5;39m`, // blue
|
|
41
|
+
claude: `${ESC}[38;2;232;112;64m`, // orange
|
|
42
|
+
triflux: `${ESC}[38;5;214m`, // amber
|
|
43
|
+
accent: `${ESC}[38;5;75m`, // light blue (Catppuccin blue)
|
|
44
|
+
muted: `${ESC}[38;5;245m`, // gray
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const BG = {
|
|
48
|
+
black: `${ESC}[40m`,
|
|
49
|
+
red: `${ESC}[41m`,
|
|
50
|
+
green: `${ESC}[42m`,
|
|
51
|
+
yellow: `${ESC}[43m`,
|
|
52
|
+
blue: `${ESC}[44m`,
|
|
53
|
+
header: `${ESC}[48;5;236m`, // dark gray
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// ── 색상 헬퍼 ──
|
|
57
|
+
export function color(text, fg, bg) {
|
|
58
|
+
const prefix = (fg || "") + (bg || "");
|
|
59
|
+
return prefix ? `${prefix}${text}${RESET}` : text;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function bold(text) { return `${BOLD}${text}${RESET}`; }
|
|
63
|
+
export function dim(text) { return `${DIM}${text}${RESET}`; }
|
|
64
|
+
|
|
65
|
+
// ── 박스 그리기 (유니코드 테두리) ──
|
|
66
|
+
const BOX = { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│", ml: "├", mr: "┤" };
|
|
67
|
+
|
|
68
|
+
export function box(lines, width) {
|
|
69
|
+
const top = `${BOX.tl}${BOX.h.repeat(width - 2)}${BOX.tr}`;
|
|
70
|
+
const bot = `${BOX.bl}${BOX.h.repeat(width - 2)}${BOX.br}`;
|
|
71
|
+
const mid = `${BOX.ml}${BOX.h.repeat(width - 2)}${BOX.mr}`;
|
|
72
|
+
const body = lines.map((l) => `${BOX.v} ${padRight(l, width - 4)} ${BOX.v}`);
|
|
73
|
+
return { top, body, bot, mid };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── 텍스트 유틸 ──
|
|
77
|
+
export function padRight(str, len) {
|
|
78
|
+
// ANSI 코드 제외한 실제 표시 길이 기준 패딩
|
|
79
|
+
const visible = stripAnsi(str);
|
|
80
|
+
const pad = Math.max(0, len - visible.length);
|
|
81
|
+
return str + " ".repeat(pad);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function truncate(str, maxLen) {
|
|
85
|
+
const visible = stripAnsi(str);
|
|
86
|
+
if (visible.length <= maxLen) return str;
|
|
87
|
+
// 간단한 잘라내기 (ANSI 코드 포함 시 근사치)
|
|
88
|
+
return str.slice(0, maxLen - 1) + "…";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function stripAnsi(str) {
|
|
92
|
+
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── 진행률 바 ──
|
|
96
|
+
export function progressBar(ratio, width = 20) {
|
|
97
|
+
const filled = Math.round(ratio * width);
|
|
98
|
+
const empty = width - filled;
|
|
99
|
+
return `${FG.accent}${"█".repeat(filled)}${FG.muted}${"░".repeat(empty)}${RESET}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── 상태 아이콘 ──
|
|
103
|
+
export const STATUS_ICON = {
|
|
104
|
+
running: `${FG.blue}⏳${RESET}`,
|
|
105
|
+
completed: `${FG.green}✓${RESET}`,
|
|
106
|
+
failed: `${FG.red}✗${RESET}`,
|
|
107
|
+
pending: `${FG.gray}⏸${RESET}`,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const CLI_ICON = {
|
|
111
|
+
codex: `${FG.codex}⚪${RESET}`,
|
|
112
|
+
gemini: `${FG.gemini}🔵${RESET}`,
|
|
113
|
+
claude: `${FG.claude}🟠${RESET}`,
|
|
114
|
+
};
|
|
@@ -11,6 +11,7 @@ export function parseTeamArgs(args = []) {
|
|
|
11
11
|
let progressive = true;
|
|
12
12
|
let timeoutSec = 300;
|
|
13
13
|
let verbose = false;
|
|
14
|
+
let dashboard = false;
|
|
14
15
|
let mcpProfile = "";
|
|
15
16
|
|
|
16
17
|
for (let index = 0; index < args.length; index += 1) {
|
|
@@ -35,6 +36,8 @@ export function parseTeamArgs(args = []) {
|
|
|
35
36
|
autoAttach = false;
|
|
36
37
|
} else if (current === "--verbose") {
|
|
37
38
|
verbose = true;
|
|
39
|
+
} else if (current === "--dashboard") {
|
|
40
|
+
dashboard = true;
|
|
38
41
|
} else if (current === "--no-progressive") {
|
|
39
42
|
progressive = false;
|
|
40
43
|
} else if (current === "--timeout" && args[index + 1]) {
|
|
@@ -57,6 +60,7 @@ export function parseTeamArgs(args = []) {
|
|
|
57
60
|
progressive,
|
|
58
61
|
timeoutSec,
|
|
59
62
|
verbose,
|
|
63
|
+
dashboard,
|
|
60
64
|
mcpProfile,
|
|
61
65
|
};
|
|
62
66
|
}
|
|
@@ -1,38 +1,61 @@
|
|
|
1
1
|
import { BOLD, DIM, GREEN, RESET, AMBER } from "../../../shared.mjs";
|
|
2
2
|
import { runHeadlessInteractive, resolveCliType } from "../../../headless.mjs";
|
|
3
|
+
import { createTui } from "../../../tui.mjs";
|
|
3
4
|
import { ok, warn } from "../../render.mjs";
|
|
4
5
|
import { buildTasks } from "../../services/task-model.mjs";
|
|
5
6
|
import { clearTeamState } from "../../services/state-store.mjs";
|
|
6
7
|
|
|
7
|
-
export async function startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec, verbose, mcpProfile }) {
|
|
8
|
+
export async function startHeadlessTeam({ sessionId, task, lead, agents, subtasks, layout, assigns, autoAttach, progressive, timeoutSec, verbose, dashboard, mcpProfile }) {
|
|
8
9
|
// --assign이 있으면 그것을 사용, 없으면 agents+subtasks 조합
|
|
9
10
|
const assignments = assigns && assigns.length > 0
|
|
10
11
|
? assigns.map((a, i) => ({ cli: resolveCliType(a.cli), prompt: a.prompt, role: a.role || `worker-${i + 1}`, mcp: mcpProfile }))
|
|
11
12
|
: subtasks.map((subtask, i) => ({ cli: resolveCliType(agents[i] || agents[0]), prompt: subtask, role: `worker-${i + 1}`, mcp: mcpProfile }));
|
|
12
13
|
|
|
14
|
+
const startedAt = Date.now();
|
|
13
15
|
ok(`headless ${assignments.length}워커 시작`);
|
|
14
16
|
|
|
17
|
+
// TUI 대시보드 (--dashboard 플래그)
|
|
18
|
+
const tui = dashboard ? createTui({ refreshMs: 1000 }) : null;
|
|
19
|
+
|
|
15
20
|
const handle = await runHeadlessInteractive(sessionId, assignments, {
|
|
16
21
|
timeoutSec: timeoutSec || 300,
|
|
17
22
|
layout,
|
|
18
23
|
autoAttach: !!autoAttach,
|
|
19
24
|
progressive: progressive !== false,
|
|
20
|
-
progressIntervalSec: verbose ? 10 : 0,
|
|
21
|
-
onProgress:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
progressIntervalSec: (verbose || dashboard) ? 10 : 0,
|
|
26
|
+
onProgress: function onProgress(event) {
|
|
27
|
+
// TUI 대시보드 업데이트
|
|
28
|
+
if (tui) {
|
|
29
|
+
if (event.type === "dispatched") {
|
|
30
|
+
tui.updateWorker(event.paneName, { cli: event.cli, status: "running" });
|
|
31
|
+
} else if (event.type === "progress") {
|
|
32
|
+
const snap = (event.snapshot || "").split("\n").filter(l => l.trim()).pop() || "";
|
|
33
|
+
tui.updateWorker(event.paneName, { snapshot: snap });
|
|
34
|
+
} else if (event.type === "completed") {
|
|
35
|
+
tui.updateWorker(event.paneName, {
|
|
36
|
+
status: event.matched && event.exitCode === 0 ? "completed" : "failed",
|
|
37
|
+
elapsed: Math.round((Date.now() - (tui._start || Date.now())) / 1000),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// verbose 텍스트 출력 (TUI 비활성 시)
|
|
43
|
+
if (verbose && !tui) {
|
|
44
|
+
if (event.type === "session_created") {
|
|
45
|
+
console.log(` ${DIM}세션: ${event.sessionName}${RESET}`);
|
|
46
|
+
} else if (event.type === "worker_added") {
|
|
47
|
+
console.log(` ${DIM}[+] ${event.paneTitle}${RESET}`);
|
|
48
|
+
} else if (event.type === "dispatched") {
|
|
49
|
+
console.log(` ${DIM}[${event.paneName}] ${event.cli} dispatch${RESET}`);
|
|
50
|
+
} else if (event.type === "progress") {
|
|
51
|
+
const last = (event.snapshot || "").split("\n").filter(l => l.trim()).pop() || "";
|
|
52
|
+
if (last) console.log(` ${DIM}[${event.paneName}] ${last.slice(0, 60)}${RESET}`);
|
|
53
|
+
} else if (event.type === "completed") {
|
|
54
|
+
const icon = event.matched && event.exitCode === 0 ? `${GREEN}✓${RESET}` : `${AMBER}✗${RESET}`;
|
|
55
|
+
console.log(` ${icon} [${event.paneName}] ${event.cli} exit=${event.exitCode}${event.sessionDead ? " (dead)" : ""}`);
|
|
56
|
+
}
|
|
34
57
|
}
|
|
35
|
-
}
|
|
58
|
+
},
|
|
36
59
|
});
|
|
37
60
|
|
|
38
61
|
// 최소 결과 요약
|
|
@@ -71,6 +94,21 @@ export async function startHeadlessTeam({ sessionId, task, lead, agents, subtask
|
|
|
71
94
|
}
|
|
72
95
|
}
|
|
73
96
|
|
|
97
|
+
// TUI에 handoff 최종 반영
|
|
98
|
+
if (tui) {
|
|
99
|
+
for (const r of results) {
|
|
100
|
+
if (r.handoff) {
|
|
101
|
+
tui.updateWorker(r.paneName, {
|
|
102
|
+
status: r.matched && r.exitCode === 0 ? "completed" : "failed",
|
|
103
|
+
handoff: r.handoff,
|
|
104
|
+
elapsed: Math.round((Date.now() - startedAt) / 1000),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
tui.render(); // 최종 프레임
|
|
109
|
+
tui.close();
|
|
110
|
+
}
|
|
111
|
+
|
|
74
112
|
// 세션 정리
|
|
75
113
|
handle.kill();
|
|
76
114
|
|
package/hub/team/tui.mjs
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// hub/team/tui.mjs — Zero-dependency TUI 대시보드 (v7.0)
|
|
2
|
+
// handoff 데이터 기반 워커 상태 실시간 ANSI 렌더링
|
|
3
|
+
// 외부 의존성 0 — Node.js 내장 + ansi.mjs만 사용
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
cursorHome, clearLine, cursorHide, cursorShow,
|
|
7
|
+
RESET, BOLD, DIM, FG, BG,
|
|
8
|
+
color, dim, truncate, stripAnsi,
|
|
9
|
+
STATUS_ICON, CLI_ICON,
|
|
10
|
+
} from "./ansi.mjs";
|
|
11
|
+
|
|
12
|
+
const VERSION = "7.0.0";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* TUI 대시보드 생성
|
|
16
|
+
* @param {object} [opts]
|
|
17
|
+
* @param {NodeJS.WriteStream} [opts.stream=process.stdout]
|
|
18
|
+
* @param {number} [opts.refreshMs=1000] — 자동 갱신 주기 (0=수동만)
|
|
19
|
+
* @param {number} [opts.width=0] — 0이면 터미널 폭 자동
|
|
20
|
+
* @returns {TuiHandle}
|
|
21
|
+
*/
|
|
22
|
+
export function createTui(opts = {}) {
|
|
23
|
+
const {
|
|
24
|
+
stream = process.stdout,
|
|
25
|
+
refreshMs = 1000,
|
|
26
|
+
width: fixedWidth = 0,
|
|
27
|
+
} = opts;
|
|
28
|
+
|
|
29
|
+
const workers = new Map();
|
|
30
|
+
let pipeline = { phase: "exec", fix_attempt: 0 };
|
|
31
|
+
let startedAt = Date.now();
|
|
32
|
+
let timer = null;
|
|
33
|
+
let closed = false;
|
|
34
|
+
let frameCount = 0;
|
|
35
|
+
|
|
36
|
+
function w() { return fixedWidth > 0 ? fixedWidth : (stream.columns || 80); }
|
|
37
|
+
function out(text) { if (!closed) stream.write(text); }
|
|
38
|
+
|
|
39
|
+
// ── 렌더 함수 ──
|
|
40
|
+
|
|
41
|
+
function header() {
|
|
42
|
+
const elapsed = Math.round((Date.now() - startedAt) / 1000);
|
|
43
|
+
const vals = [...workers.values()];
|
|
44
|
+
const done = vals.filter((s) => s.status === "completed").length;
|
|
45
|
+
const fail = vals.filter((s) => s.status === "failed").length;
|
|
46
|
+
const run = vals.filter((s) => s.status === "running").length;
|
|
47
|
+
|
|
48
|
+
const left = ` ${color("▲ triflux", FG.triflux)} ${dim(`v${VERSION}`)}`;
|
|
49
|
+
const right = `${color(`${done}✓`, FG.green)} ${color(`${fail}✗`, FG.red)} ${run}⏳ ${dim(`${vals.length}총`)} ${dim(`${elapsed}s`)}`;
|
|
50
|
+
const gap = Math.max(1, w() - stripAnsi(left).length - stripAnsi(right).length - 1);
|
|
51
|
+
return `${BG.header}${left}${" ".repeat(gap)}${right}${RESET}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function workerCard(name, st) {
|
|
55
|
+
const icon = CLI_ICON[st.cli] || dim("●");
|
|
56
|
+
const sIcon = STATUS_ICON[st.status] || STATUS_ICON.pending;
|
|
57
|
+
const el = st.elapsed != null ? `${st.elapsed}s` : "";
|
|
58
|
+
const role = st.role ? dim(`(${st.role})`) : "";
|
|
59
|
+
const conf = st.handoff?.confidence ? dim(` ${st.handoff.confidence}`) : "";
|
|
60
|
+
|
|
61
|
+
const lines = [];
|
|
62
|
+
lines.push(` ${icon} ${st.cli} ${role} ${sIcon} ${el}${conf}`);
|
|
63
|
+
|
|
64
|
+
if (st.handoff?.verdict) {
|
|
65
|
+
lines.push(` ${truncate(st.handoff.verdict, w() - 8)}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (st.handoff?.files_changed?.length) {
|
|
69
|
+
const f = st.handoff.files_changed.slice(0, 3).join(", ");
|
|
70
|
+
const more = st.handoff.files_changed.length > 3 ? ` +${st.handoff.files_changed.length - 3}` : "";
|
|
71
|
+
lines.push(` ${dim(`files: ${f}${more}`)}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (st.status === "failed" && st.handoff) {
|
|
75
|
+
const p = [];
|
|
76
|
+
if (st.handoff.risk) p.push(`risk:${st.handoff.risk}`);
|
|
77
|
+
if (st.handoff.lead_action) p.push(`action:${st.handoff.lead_action}`);
|
|
78
|
+
if (p.length) lines.push(` ${color(p.join(" │ "), FG.red)}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (st.status === "running" && st.snapshot) {
|
|
82
|
+
lines.push(` ${dim(truncate(st.snapshot, w() - 8))}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return lines;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function pipelineBar() {
|
|
89
|
+
const phases = ["plan", "prd", "confidence", "exec", "deslop", "verify", "selfcheck", "complete"];
|
|
90
|
+
const cur = pipeline.phase;
|
|
91
|
+
const parts = phases.map((p) => {
|
|
92
|
+
const ci = phases.indexOf(cur);
|
|
93
|
+
const pi = phases.indexOf(p);
|
|
94
|
+
if (p === cur) return `${FG.accent}${BOLD}[${p}]${RESET}`;
|
|
95
|
+
if (pi < ci) return `${FG.green}${p}${RESET}`;
|
|
96
|
+
return dim(p);
|
|
97
|
+
});
|
|
98
|
+
return ` ${parts.join(dim("→"))}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function footer() {
|
|
102
|
+
const vals = [...workers.values()];
|
|
103
|
+
const done = vals.filter((s) => s.status === "completed").length;
|
|
104
|
+
const tokenSaved = done * 850;
|
|
105
|
+
return `${BG.header} ${done}✓ 완료 │ ~${tokenSaved} 토큰 절감 ${RESET}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function render() {
|
|
109
|
+
if (closed) return;
|
|
110
|
+
frameCount++;
|
|
111
|
+
const sep = dim("─".repeat(w()));
|
|
112
|
+
const buf = [cursorHome];
|
|
113
|
+
|
|
114
|
+
buf.push(header() + clearLine + "\n");
|
|
115
|
+
buf.push(sep + "\n");
|
|
116
|
+
|
|
117
|
+
if (workers.size === 0) {
|
|
118
|
+
buf.push(dim(" (워커 대기 중...)\n"));
|
|
119
|
+
} else {
|
|
120
|
+
for (const [name, st] of workers) {
|
|
121
|
+
for (const l of workerCard(name, st)) buf.push(l + clearLine + "\n");
|
|
122
|
+
buf.push("\n");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
buf.push(sep + "\n");
|
|
127
|
+
buf.push(pipelineBar() + clearLine + "\n");
|
|
128
|
+
buf.push(footer() + clearLine + "\n");
|
|
129
|
+
|
|
130
|
+
out(buf.join(""));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 자동 갱신
|
|
134
|
+
if (refreshMs > 0) {
|
|
135
|
+
timer = setInterval(render, refreshMs);
|
|
136
|
+
if (timer.unref) timer.unref();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
updateWorker(paneName, state) {
|
|
141
|
+
const existing = workers.get(paneName) || { cli: "codex", status: "pending" };
|
|
142
|
+
workers.set(paneName, { ...existing, ...state });
|
|
143
|
+
},
|
|
144
|
+
updatePipeline(state) { pipeline = { ...pipeline, ...state }; },
|
|
145
|
+
setStartTime(ms) { startedAt = ms; },
|
|
146
|
+
render,
|
|
147
|
+
getWorkers() { return new Map(workers); },
|
|
148
|
+
getFrameCount() { return frameCount; },
|
|
149
|
+
close() {
|
|
150
|
+
if (closed) return;
|
|
151
|
+
closed = true;
|
|
152
|
+
if (timer) clearInterval(timer);
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
package/package.json
CHANGED
|
@@ -50,7 +50,8 @@ function isPsmuxInstalled() {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* tfx-route.sh 명령에서 agent, prompt
|
|
53
|
+
* tfx-route.sh 명령에서 agent, prompt, mcp, 추가 플래그를 파싱한다.
|
|
54
|
+
* v3: 손실 없는 파싱 — 원본 명령의 모든 플래그를 보존.
|
|
54
55
|
*/
|
|
55
56
|
function parseRouteCommand(cmd) {
|
|
56
57
|
const MCP_PROFILES = ["implement", "analyze", "review", "docs"];
|
|
@@ -79,7 +80,17 @@ function parseRouteCommand(cmd) {
|
|
|
79
80
|
.replace(/'"'"'/g, "'")
|
|
80
81
|
.trim();
|
|
81
82
|
|
|
82
|
-
|
|
83
|
+
// v3: 원본 명령에서 추가 플래그 추출
|
|
84
|
+
const flags = {};
|
|
85
|
+
const timeoutMatch = cmd.match(/(?:^|\s)(\d{2,4})(?:\s|$)/); // 4번째 인자 (timeout)
|
|
86
|
+
if (timeoutMatch) flags.timeout = parseInt(timeoutMatch[1], 10);
|
|
87
|
+
|
|
88
|
+
// 환경변수 기반 글로벌 플래그
|
|
89
|
+
if (process.env.TFX_DASHBOARD === "1") flags.dashboard = true;
|
|
90
|
+
if (process.env.TFX_VERBOSE === "1") flags.verbose = true;
|
|
91
|
+
if (process.env.TFX_NO_AUTO_ATTACH === "1") flags.noAutoAttach = true;
|
|
92
|
+
|
|
93
|
+
return { agent, prompt, mcp, flags };
|
|
83
94
|
}
|
|
84
95
|
|
|
85
96
|
function autoRoute(updatedCommand, reason) {
|
|
@@ -143,10 +154,21 @@ async function main() {
|
|
|
143
154
|
if (parsed) {
|
|
144
155
|
const safePrompt = parsed.prompt.replace(/'/g, "'\\''");
|
|
145
156
|
const VALID_MCP = new Set(["implement", "analyze", "review", "docs"]);
|
|
146
|
-
const
|
|
157
|
+
const f = parsed.flags || {};
|
|
158
|
+
|
|
159
|
+
// v3: 플래그 빌더 — 하드코딩 제거, 원본 의도 보존
|
|
160
|
+
const parts = ["tfx multi --teammate-mode headless"];
|
|
161
|
+
if (!f.noAutoAttach) parts.push("--auto-attach");
|
|
162
|
+
if (f.dashboard) parts.push("--dashboard");
|
|
163
|
+
if (f.verbose) parts.push("--verbose");
|
|
164
|
+
parts.push(`--assign '${parsed.agent}:${safePrompt}:${parsed.agent}'`);
|
|
165
|
+
if (parsed.mcp && VALID_MCP.has(parsed.mcp)) parts.push(`--mcp-profile ${parsed.mcp}`);
|
|
166
|
+
parts.push(`--timeout ${f.timeout || 600}`);
|
|
167
|
+
|
|
168
|
+
const builtCmd = parts.join(" ");
|
|
147
169
|
autoRoute(
|
|
148
|
-
|
|
149
|
-
`[headless-guard] auto-route: tfx-route.sh ${parsed.agent} → headless. mcp=${parsed.mcp}`,
|
|
170
|
+
builtCmd,
|
|
171
|
+
`[headless-guard] auto-route: tfx-route.sh ${parsed.agent} → headless. mcp=${parsed.mcp} dashboard=${!!f.dashboard}`,
|
|
150
172
|
);
|
|
151
173
|
}
|
|
152
174
|
deny(
|