triflux 9.2.0 → 9.2.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 +10 -55
- package/hub/team/dashboard-layout.mjs +2 -0
- package/hub/team/headless.mjs +6 -2
- package/hub/team/tui-lite.mjs +276 -0
- package/hub/team/tui-viewer.mjs +3 -1
- package/package.json +1 -1
- package/scripts/lib/gemini-profiles.mjs +85 -0
- package/scripts/setup.mjs +7 -51
- package/tui/setup.mjs +9 -15
package/bin/triflux.mjs
CHANGED
|
@@ -11,28 +11,16 @@ import { detectMultiplexer, getSessionAttachedCount, killSession, listSessions,
|
|
|
11
11
|
import { forceCleanupTeam } from "../hub/team/nativeProxy.mjs";
|
|
12
12
|
import { cleanupStaleOmcTeams, inspectStaleOmcTeams } from "../hub/team/staleState.mjs";
|
|
13
13
|
import { getPipelineStateDbPath } from "../hub/pipeline/state.mjs";
|
|
14
|
+
import { ensureGeminiProfiles } from "../scripts/lib/gemini-profiles.mjs";
|
|
14
15
|
|
|
15
16
|
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
16
17
|
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
17
18
|
const CODEX_DIR = join(homedir(), ".codex");
|
|
18
19
|
const CODEX_CONFIG_PATH = join(CODEX_DIR, "config.toml");
|
|
19
|
-
const GEMINI_DIR = join(homedir(), ".gemini");
|
|
20
|
-
const GEMINI_PROFILES_PATH = join(GEMINI_DIR, "triflux-profiles.json");
|
|
21
20
|
const PKG = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
22
21
|
|
|
23
22
|
// 이 배열에 포함된 버전에서만 star prompt를 표시한다 (빈 배열 = 모든 버전에서 표시)
|
|
24
|
-
const STAR_PROMPT_VERSIONS = ["9.2.
|
|
25
|
-
|
|
26
|
-
const DEFAULT_GEMINI_PROFILES = {
|
|
27
|
-
model: "gemini-3.1-pro-preview",
|
|
28
|
-
profiles: {
|
|
29
|
-
pro31: { model: "gemini-3.1-pro-preview", hint: "3.1 Pro — 플래그십 (1M ctx, 멀티모달)" },
|
|
30
|
-
flash3: { model: "gemini-3-flash-preview", hint: "3.0 Flash — 빠른 응답, 비용 효율" },
|
|
31
|
-
pro25: { model: "gemini-2.5-pro", hint: "2.5 Pro — 안정 (추론 강화)" },
|
|
32
|
-
flash25: { model: "gemini-2.5-flash", hint: "2.5 Flash — 경량 범용" },
|
|
33
|
-
lite25: { model: "gemini-2.5-flash-lite", hint: "2.5 Flash Lite — 최경량" },
|
|
34
|
-
},
|
|
35
|
-
};
|
|
23
|
+
const STAR_PROMPT_VERSIONS = ["9.2.1"];
|
|
36
24
|
|
|
37
25
|
const REQUIRED_CODEX_PROFILES = [
|
|
38
26
|
{
|
|
@@ -512,44 +500,6 @@ function previewCodexProfiles() {
|
|
|
512
500
|
};
|
|
513
501
|
}
|
|
514
502
|
|
|
515
|
-
function ensureGeminiProfiles() {
|
|
516
|
-
try {
|
|
517
|
-
if (!existsSync(GEMINI_DIR)) mkdirSync(GEMINI_DIR, { recursive: true });
|
|
518
|
-
|
|
519
|
-
if (!existsSync(GEMINI_PROFILES_PATH)) {
|
|
520
|
-
writeFileSync(GEMINI_PROFILES_PATH, JSON.stringify(DEFAULT_GEMINI_PROFILES, null, 2) + "\n", "utf8");
|
|
521
|
-
return { ok: true, created: true, count: Object.keys(DEFAULT_GEMINI_PROFILES.profiles).length };
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
let cfg;
|
|
525
|
-
try {
|
|
526
|
-
cfg = JSON.parse(readFileSync(GEMINI_PROFILES_PATH, "utf8"));
|
|
527
|
-
} catch {
|
|
528
|
-
// 파싱 실패 → 재생성
|
|
529
|
-
writeFileSync(GEMINI_PROFILES_PATH, JSON.stringify(DEFAULT_GEMINI_PROFILES, null, 2) + "\n", "utf8");
|
|
530
|
-
return { ok: true, created: true, count: Object.keys(DEFAULT_GEMINI_PROFILES.profiles).length };
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
if (!cfg.profiles) cfg.profiles = {};
|
|
534
|
-
let added = 0;
|
|
535
|
-
for (const [name, value] of Object.entries(DEFAULT_GEMINI_PROFILES.profiles)) {
|
|
536
|
-
if (!cfg.profiles[name]) {
|
|
537
|
-
cfg.profiles[name] = value;
|
|
538
|
-
added++;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
if (!cfg.model) cfg.model = DEFAULT_GEMINI_PROFILES.model;
|
|
542
|
-
|
|
543
|
-
if (added > 0) {
|
|
544
|
-
writeFileSync(GEMINI_PROFILES_PATH, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
return { ok: true, created: false, added, count: Object.keys(cfg.profiles).length };
|
|
548
|
-
} catch (e) {
|
|
549
|
-
return { ok: false, message: e.message };
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
503
|
function syncFile(src, dst, label) {
|
|
554
504
|
const dstDir = dirname(dst);
|
|
555
505
|
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
@@ -716,6 +666,11 @@ function getSetupSyncTargets() {
|
|
|
716
666
|
dst: join(CLAUDE_DIR, "scripts", "lib", "keyword-rules.mjs"),
|
|
717
667
|
label: "lib/keyword-rules.mjs",
|
|
718
668
|
},
|
|
669
|
+
{
|
|
670
|
+
src: join(PKG_ROOT, "scripts", "lib", "gemini-profiles.mjs"),
|
|
671
|
+
dst: join(CLAUDE_DIR, "scripts", "lib", "gemini-profiles.mjs"),
|
|
672
|
+
label: "lib/gemini-profiles.mjs",
|
|
673
|
+
},
|
|
719
674
|
{
|
|
720
675
|
src: join(PKG_ROOT, "scripts", "tfx-route-worker.mjs"),
|
|
721
676
|
dst: join(CLAUDE_DIR, "scripts", "tfx-route-worker.mjs"),
|
|
@@ -870,7 +825,7 @@ function buildSetupDryRunPlan() {
|
|
|
870
825
|
}
|
|
871
826
|
|
|
872
827
|
function cmdSetup(options = {}) {
|
|
873
|
-
const { dryRun = false } = options;
|
|
828
|
+
const { dryRun = false, fromUpdate = false } = options;
|
|
874
829
|
if (dryRun) {
|
|
875
830
|
printJson(buildSetupDryRunPlan());
|
|
876
831
|
return;
|
|
@@ -1024,7 +979,7 @@ function cmdSetup(options = {}) {
|
|
|
1024
979
|
}
|
|
1025
980
|
|
|
1026
981
|
// Star request (버전 게이팅)
|
|
1027
|
-
const showStar = STAR_PROMPT_VERSIONS.length === 0 || STAR_PROMPT_VERSIONS.includes(PKG.version);
|
|
982
|
+
const showStar = !fromUpdate && (STAR_PROMPT_VERSIONS.length === 0 || STAR_PROMPT_VERSIONS.includes(PKG.version));
|
|
1028
983
|
if (showStar) {
|
|
1029
984
|
try {
|
|
1030
985
|
execFileSync("gh", ["auth", "status"], { timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -1987,7 +1942,7 @@ function cmdUpdate() {
|
|
|
1987
1942
|
|
|
1988
1943
|
// setup 재실행 — 개선된 cmdSetup()이 Gemini 프로필, CLI 확인, 요약 테이블 포함
|
|
1989
1944
|
console.log(`\n${CYAN}── 설정 동기화 ──${RESET}`);
|
|
1990
|
-
cmdSetup();
|
|
1945
|
+
cmdSetup({ fromUpdate: true });
|
|
1991
1946
|
|
|
1992
1947
|
if (stoppedHubInfo) {
|
|
1993
1948
|
if (startHubAfterUpdate(stoppedHubInfo)) ok("hub 재기동 완료");
|
|
@@ -3,6 +3,7 @@ const USER_DASHBOARD_LAYOUTS = new Set([
|
|
|
3
3
|
"split-2col",
|
|
4
4
|
"split-3col",
|
|
5
5
|
"auto",
|
|
6
|
+
"lite",
|
|
6
7
|
]);
|
|
7
8
|
|
|
8
9
|
const DASHBOARD_LAYOUTS = new Set([
|
|
@@ -23,6 +24,7 @@ export function parseDashboardLayout(value) {
|
|
|
23
24
|
|
|
24
25
|
export function resolveDashboardLayout(value, workerCount = 0) {
|
|
25
26
|
const normalized = normalizeDashboardLayout(value, { allowAuto: true });
|
|
27
|
+
if (normalized === "lite") return "lite";
|
|
26
28
|
if (normalized !== "auto") return normalized;
|
|
27
29
|
if (workerCount >= 4) return "summary+detail";
|
|
28
30
|
if (workerCount === 3) return "split-3col";
|
package/hub/team/headless.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { getBackend } from "./backend.mjs";
|
|
|
24
24
|
import { resolveDashboardLayout } from "./dashboard-layout.mjs";
|
|
25
25
|
import { normalizeDashboardAnchor } from "./dashboard-anchor.mjs";
|
|
26
26
|
import { createLogDashboard } from "./tui.mjs";
|
|
27
|
+
import { createLiteDashboard } from "./tui-lite.mjs";
|
|
27
28
|
|
|
28
29
|
const RESULT_DIR = join(tmpdir(), "tfx-headless");
|
|
29
30
|
|
|
@@ -357,12 +358,15 @@ export async function runHeadless(sessionName, assignments, opts = {}) {
|
|
|
357
358
|
let tui = null;
|
|
358
359
|
const resolvedLayout = resolveDashboardLayout(dashboardLayout, assignments.length);
|
|
359
360
|
if (dashboard && process.stdout.isTTY) {
|
|
360
|
-
|
|
361
|
+
const dashOpts = {
|
|
361
362
|
stream: process.stdout,
|
|
362
363
|
input: process.stdin,
|
|
363
364
|
refreshMs: 200,
|
|
364
365
|
layout: resolvedLayout,
|
|
365
|
-
}
|
|
366
|
+
};
|
|
367
|
+
tui = resolvedLayout === "lite"
|
|
368
|
+
? createLiteDashboard(dashOpts)
|
|
369
|
+
: createLogDashboard(dashOpts);
|
|
366
370
|
tui.setStartTime(Date.now());
|
|
367
371
|
// 초기 워커 상태 등록
|
|
368
372
|
for (let i = 0; i < assignments.length; i++) {
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { altScreenOff, altScreenOn, BG, bold, box, clearScreen, color, cursorHide, cursorHome, cursorShow, dim, FG, MOCHA, padRight, progressBar, statusBadge, stripAnsi, truncate, wcswidth } from "./ansi.mjs";
|
|
2
|
+
|
|
3
|
+
const FALLBACK_COLUMNS = 100, FALLBACK_ROWS = 24;
|
|
4
|
+
const VALID_TABS = new Set(["log", "detail", "files"]);
|
|
5
|
+
|
|
6
|
+
let VERSION = "lite";
|
|
7
|
+
try { const { createRequire } = await import("node:module"); VERSION = createRequire(import.meta.url)("../../package.json").version; } catch {}
|
|
8
|
+
|
|
9
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
10
|
+
|
|
11
|
+
function sanitizeBlock(text, rawMode = false) {
|
|
12
|
+
const value = String(text || "").replace(/\r/g, "");
|
|
13
|
+
const cleaned = rawMode
|
|
14
|
+
? value
|
|
15
|
+
: value
|
|
16
|
+
.replace(/```[\s\S]*?(?:```|$)/g, "\n")
|
|
17
|
+
.replace(/^\s*```.*$/gm, "")
|
|
18
|
+
.replace(/^(?:PS\s+\S[^\n]*?>|>\s+|\$\s+)[^\n]*/gm, "");
|
|
19
|
+
return cleaned
|
|
20
|
+
.split("\n")
|
|
21
|
+
.map((line) => line.trim())
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.filter((line) => line !== "--- HANDOFF ---")
|
|
24
|
+
.join("\n")
|
|
25
|
+
.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sanitizeOneLine(text, fallback = "") {
|
|
29
|
+
return sanitizeBlock(text).replace(/\s+/g, " ").trim() || fallback;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sanitizeFiles(files) {
|
|
33
|
+
const list = Array.isArray(files) ? files : String(files || "").split(",");
|
|
34
|
+
return list.map((entry) => sanitizeOneLine(entry)).filter(Boolean);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeTokens(tokens) {
|
|
38
|
+
if (tokens === null || tokens === undefined || tokens === "") return "";
|
|
39
|
+
if (typeof tokens === "number" && Number.isFinite(tokens)) return tokens;
|
|
40
|
+
const raw = sanitizeOneLine(tokens);
|
|
41
|
+
const match = raw.match(/(\d+(?:[.,]\d+)?\s*[kKmM]?)/);
|
|
42
|
+
return match ? match[1].replace(/\s+/g, "").toLowerCase() : raw;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function formatTokens(tokens) {
|
|
46
|
+
if (!tokens && tokens !== 0) return "n/a";
|
|
47
|
+
if (typeof tokens === "number") {
|
|
48
|
+
if (tokens >= 1_000_000) return `${(tokens / 1_000_000).toFixed(1)}m`;
|
|
49
|
+
if (tokens >= 1_000) return `${(tokens / 1_000).toFixed(1)}k`;
|
|
50
|
+
}
|
|
51
|
+
return String(tokens);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function wrap(text, width) {
|
|
55
|
+
const limit = Math.max(8, width);
|
|
56
|
+
const lines = [];
|
|
57
|
+
for (const rawLine of sanitizeBlock(text).split("\n")) {
|
|
58
|
+
const words = rawLine.split(/\s+/).filter(Boolean);
|
|
59
|
+
if (words.length === 0) continue;
|
|
60
|
+
let current = "";
|
|
61
|
+
for (const word of words) {
|
|
62
|
+
const next = current ? `${current} ${word}` : word;
|
|
63
|
+
if (wcswidth(next) <= limit) {
|
|
64
|
+
current = next;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (current) lines.push(current);
|
|
68
|
+
current = word;
|
|
69
|
+
while (wcswidth(current) > limit) {
|
|
70
|
+
lines.push(current.slice(0, limit));
|
|
71
|
+
current = current.slice(limit);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (current) lines.push(current);
|
|
75
|
+
}
|
|
76
|
+
return lines;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const runtimeStatus = (worker) => worker?.handoff?.status || worker?.status || "pending";
|
|
80
|
+
|
|
81
|
+
function normalizeWorkerState(existing = {}, state = {}) {
|
|
82
|
+
const handoff = state.handoff === undefined
|
|
83
|
+
? existing.handoff
|
|
84
|
+
: {
|
|
85
|
+
...(existing.handoff || {}),
|
|
86
|
+
...(state.handoff || {}),
|
|
87
|
+
verdict: state.handoff?.verdict !== undefined ? sanitizeOneLine(state.handoff.verdict) : existing.handoff?.verdict,
|
|
88
|
+
confidence: state.handoff?.confidence !== undefined ? sanitizeOneLine(state.handoff.confidence) : existing.handoff?.confidence,
|
|
89
|
+
status: state.handoff?.status !== undefined ? sanitizeOneLine(state.handoff.status) : existing.handoff?.status,
|
|
90
|
+
files_changed: state.handoff?.files_changed !== undefined ? sanitizeFiles(state.handoff.files_changed) : existing.handoff?.files_changed,
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
...existing,
|
|
94
|
+
...state,
|
|
95
|
+
cli: state.cli !== undefined ? sanitizeOneLine(state.cli, existing.cli || "codex") : (existing.cli || "codex"),
|
|
96
|
+
status: state.status !== undefined ? sanitizeOneLine(state.status, existing.status || "pending") : (existing.status || "pending"),
|
|
97
|
+
snapshot: state.snapshot !== undefined ? sanitizeBlock(state.snapshot) : existing.snapshot,
|
|
98
|
+
summary: state.summary !== undefined ? sanitizeBlock(state.summary) : existing.summary,
|
|
99
|
+
detail: state.detail !== undefined ? sanitizeBlock(state.detail) : existing.detail,
|
|
100
|
+
findings: state.findings !== undefined ? sanitizeFiles(state.findings) : existing.findings,
|
|
101
|
+
files_changed: state.files_changed !== undefined ? sanitizeFiles(state.files_changed) : existing.files_changed,
|
|
102
|
+
confidence: state.confidence !== undefined ? sanitizeOneLine(state.confidence) : existing.confidence,
|
|
103
|
+
tokens: state.tokens !== undefined ? normalizeTokens(state.tokens) : existing.tokens,
|
|
104
|
+
progress: state.progress !== undefined ? clamp(Number(state.progress) || 0, 0, 1) : existing.progress,
|
|
105
|
+
handoff,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function frame(lines, width, border = MOCHA.border) {
|
|
110
|
+
const body = lines.length ? lines : [dim("내용 없음")];
|
|
111
|
+
const rendered = box(body.map((line) => padRight(truncate(line, width - 4), width - 4)), width, border);
|
|
112
|
+
return [rendered.top, ...rendered.body, rendered.bot];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function fitHeight(lines, width, height) {
|
|
116
|
+
const out = lines.slice(0, Math.max(3, height));
|
|
117
|
+
while (out.length < Math.max(3, height)) out.push(" ".repeat(width));
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function buildHeader(width, names, workers, pipeline, startedAt) {
|
|
122
|
+
const counts = { ok: 0, partial: 0, failed: 0, running: 0 };
|
|
123
|
+
for (const name of names) {
|
|
124
|
+
const status = runtimeStatus(workers.get(name));
|
|
125
|
+
if (status === "ok" || status === "completed") counts.ok++;
|
|
126
|
+
else if (status === "partial") counts.partial++;
|
|
127
|
+
else if (status === "failed") counts.failed++;
|
|
128
|
+
else if (status === "running" || status === "in_progress") counts.running++;
|
|
129
|
+
}
|
|
130
|
+
const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
|
|
131
|
+
const line1 = color(` triflux ${VERSION} `, FG.black, BG.header)
|
|
132
|
+
+ ` ${bold(`phase ${pipeline.phase || "exec"}`)}`
|
|
133
|
+
+ ` ${dim(`+${elapsed}s`)} ${names.length} workers`;
|
|
134
|
+
const line2 = `${color(`ok ${counts.ok}`, MOCHA.ok)} ${color(`partial ${counts.partial}`, MOCHA.partial)} ${color(`failed ${counts.failed}`, MOCHA.fail)} ${color(`running ${counts.running}`, MOCHA.executing)}`;
|
|
135
|
+
return [padRight(line1, width), padRight(line2, width)];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function buildWorkerRail(names, workers, selectedWorker, width) {
|
|
139
|
+
const lines = names.length
|
|
140
|
+
? names.map((name, index) => {
|
|
141
|
+
const worker = workers.get(name);
|
|
142
|
+
const status = runtimeStatus(worker);
|
|
143
|
+
const pct = Math.round(((worker?.progress ?? (status === "completed" ? 1 : 0)) || 0) * 100);
|
|
144
|
+
const token = worker?.tokens ? ` tok ${formatTokens(worker.tokens)}` : "";
|
|
145
|
+
const prefix = name === selectedWorker ? color("▶", MOCHA.blue) : dim("·");
|
|
146
|
+
return `${prefix} ${index + 1}.${name} ${stripAnsi(statusBadge(status))} ${pct}%${token}`;
|
|
147
|
+
})
|
|
148
|
+
: [dim("workers 없음")];
|
|
149
|
+
return frame(lines, width);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function buildDetail(workerName, worker, width, tab, helpVisible) {
|
|
153
|
+
if (helpVisible) {
|
|
154
|
+
return frame(
|
|
155
|
+
[
|
|
156
|
+
bold("tui-lite"),
|
|
157
|
+
"selectWorker(name) / setFocusTab(tab) / render()",
|
|
158
|
+
"tabs: log | detail | files",
|
|
159
|
+
"toggleDetail(false) 로 상세 패널 숨김",
|
|
160
|
+
],
|
|
161
|
+
width,
|
|
162
|
+
MOCHA.blue,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (!workerName || !worker) return frame([dim("선택된 워커 없음")], width);
|
|
166
|
+
const detailLines = [
|
|
167
|
+
bold(workerName),
|
|
168
|
+
`status ${runtimeStatus(worker)}`,
|
|
169
|
+
`progress ${Math.round((worker.progress || 0) * 100)}% ${progressBar(Math.round((worker.progress || 0) * 100), 12)}`,
|
|
170
|
+
`tokens ${formatTokens(worker.tokens)}`,
|
|
171
|
+
`confidence ${worker.handoff?.confidence || worker.confidence || "n/a"}`,
|
|
172
|
+
`verdict ${worker.handoff?.verdict || worker.summary || worker.snapshot || "-"}`,
|
|
173
|
+
];
|
|
174
|
+
if (tab === "files") {
|
|
175
|
+
const files = [...sanitizeFiles(worker.handoff?.files_changed), ...sanitizeFiles(worker.files_changed)];
|
|
176
|
+
detailLines.push(...(files.length ? files.map((file) => `files ${file}`) : ["files 없음"]));
|
|
177
|
+
} else if (tab === "detail") {
|
|
178
|
+
detailLines.push(...wrap(worker.detail || worker.summary || worker.snapshot || "", width - 4));
|
|
179
|
+
} else {
|
|
180
|
+
detailLines.push(...wrap(worker.summary || worker.snapshot || worker.detail || "", width - 4));
|
|
181
|
+
}
|
|
182
|
+
return frame(detailLines, width, MOCHA.thinking);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function createLiteDashboard(opts = {}) {
|
|
186
|
+
const {
|
|
187
|
+
stream = process.stdout,
|
|
188
|
+
refreshMs = 1000,
|
|
189
|
+
columns,
|
|
190
|
+
rows,
|
|
191
|
+
layout = "auto",
|
|
192
|
+
forceTTY = false,
|
|
193
|
+
} = opts;
|
|
194
|
+
|
|
195
|
+
const isTTY = forceTTY || !!stream?.isTTY;
|
|
196
|
+
const workers = new Map();
|
|
197
|
+
let pipeline = { phase: "exec", fix_attempt: 0 };
|
|
198
|
+
let startedAt = Date.now();
|
|
199
|
+
let timer = null;
|
|
200
|
+
let closed = false;
|
|
201
|
+
let frameCount = 0;
|
|
202
|
+
let selectedWorker = null;
|
|
203
|
+
let detailExpanded = true;
|
|
204
|
+
let focusTab = "log";
|
|
205
|
+
let helpVisible = false;
|
|
206
|
+
|
|
207
|
+
const write = (text) => { if (!closed) stream.write(text); };
|
|
208
|
+
const workerNames = () => [...workers.keys()].sort();
|
|
209
|
+
const viewportColumns = () => Math.max(48, columns || stream?.columns || process.stdout?.columns || FALLBACK_COLUMNS);
|
|
210
|
+
const viewportRows = () => Math.max(10, rows || stream?.rows || process.stdout?.rows || FALLBACK_ROWS);
|
|
211
|
+
const ensureSelection = (names) => { if (names.length && (!selectedWorker || !workers.has(selectedWorker))) selectedWorker = names[0]; };
|
|
212
|
+
|
|
213
|
+
function buildRows() {
|
|
214
|
+
const names = workerNames();
|
|
215
|
+
ensureSelection(names);
|
|
216
|
+
const width = viewportColumns();
|
|
217
|
+
const height = viewportRows();
|
|
218
|
+
const header = buildHeader(width, names, workers, pipeline, startedAt);
|
|
219
|
+
const railOnly = !detailExpanded || names.length <= 1 || width < 100 || layout === "single";
|
|
220
|
+
if (railOnly) {
|
|
221
|
+
const sections = [header, ...buildWorkerRail(names, workers, selectedWorker, width)];
|
|
222
|
+
if (detailExpanded) sections.push(...buildDetail(selectedWorker, workers.get(selectedWorker), width, focusTab, helpVisible));
|
|
223
|
+
return fitHeight(sections, width, height);
|
|
224
|
+
}
|
|
225
|
+
const railWidth = Math.max(28, Math.floor(width * 0.32));
|
|
226
|
+
const detailWidth = width - railWidth - 1;
|
|
227
|
+
const bodyHeight = Math.max(6, height - header.length);
|
|
228
|
+
const rail = fitHeight(buildWorkerRail(names, workers, selectedWorker, railWidth), railWidth, bodyHeight);
|
|
229
|
+
const detail = fitHeight(buildDetail(selectedWorker, workers.get(selectedWorker), detailWidth, focusTab, helpVisible), detailWidth, bodyHeight);
|
|
230
|
+
return [
|
|
231
|
+
...header,
|
|
232
|
+
...Array.from({ length: bodyHeight }, (_, index) => `${rail[index]}${dim("│")}${detail[index]}`),
|
|
233
|
+
];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function render() {
|
|
237
|
+
if (closed) return;
|
|
238
|
+
frameCount++;
|
|
239
|
+
const rowsOut = buildRows();
|
|
240
|
+
if (isTTY) write(cursorHome + clearScreen + rowsOut.join("\n"));
|
|
241
|
+
else write(`${rowsOut.join("\n")}\n`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function close() {
|
|
245
|
+
if (closed) return;
|
|
246
|
+
if (timer) clearInterval(timer);
|
|
247
|
+
if (isTTY) write(cursorShow + altScreenOff);
|
|
248
|
+
closed = true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (isTTY) write(altScreenOn + cursorHide + clearScreen + cursorHome);
|
|
252
|
+
if (refreshMs > 0) {
|
|
253
|
+
timer = setInterval(render, refreshMs);
|
|
254
|
+
if (timer.unref) timer.unref();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
updateWorker(name, state) { workers.set(name, normalizeWorkerState(workers.get(name), state)); ensureSelection(workerNames()); },
|
|
259
|
+
updatePipeline(state) { pipeline = { ...pipeline, ...state }; },
|
|
260
|
+
setStartTime(ms) { startedAt = ms; },
|
|
261
|
+
selectWorker(name) { if (workers.has(name)) selectedWorker = name; },
|
|
262
|
+
toggleDetail(force) { detailExpanded = typeof force === "boolean" ? force : !detailExpanded; },
|
|
263
|
+
render,
|
|
264
|
+
getWorkers() { return new Map(workers); },
|
|
265
|
+
getFrameCount() { return frameCount; },
|
|
266
|
+
getPipelineState() { return { ...pipeline }; },
|
|
267
|
+
getSelectedWorker() { return selectedWorker; },
|
|
268
|
+
isDetailExpanded() { return detailExpanded; },
|
|
269
|
+
getFocusTab() { return focusTab; },
|
|
270
|
+
setFocusTab(tab) { if (VALID_TABS.has(tab)) focusTab = tab; },
|
|
271
|
+
getLayout() { return layout; },
|
|
272
|
+
toggleHelp(force) { helpVisible = typeof force === "boolean" ? force : !helpVisible; },
|
|
273
|
+
isHelpVisible() { return helpVisible; },
|
|
274
|
+
close,
|
|
275
|
+
};
|
|
276
|
+
}
|
package/hub/team/tui-viewer.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { execFileSync } from "node:child_process";
|
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { createLogDashboard } from "./tui.mjs";
|
|
11
|
+
import { createLiteDashboard } from "./tui-lite.mjs";
|
|
11
12
|
import { processHandoff } from "./handoff.mjs";
|
|
12
13
|
import { statusBadge } from "./ansi.mjs";
|
|
13
14
|
|
|
@@ -45,7 +46,8 @@ const MAX_BODY_BYTES = 10240;
|
|
|
45
46
|
// ── TUI 초기화 ──
|
|
46
47
|
// WT pane에서 spawn 시 process.stdout.isTTY=false일 수 있음
|
|
47
48
|
// forceTTY 시 alternate screen이 WT pane에서 렌더링 안 되는 문제 → append-only 유지
|
|
48
|
-
const
|
|
49
|
+
const tuiFactory = LAYOUT === "lite" ? createLiteDashboard : createLogDashboard;
|
|
50
|
+
const tui = tuiFactory({
|
|
49
51
|
refreshMs: 0, // render 루프를 직접 제어
|
|
50
52
|
stream: process.stdout,
|
|
51
53
|
input: process.stdin,
|
package/package.json
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_GEMINI_PROFILES = {
|
|
6
|
+
model: "gemini-3.1-pro-preview",
|
|
7
|
+
profiles: {
|
|
8
|
+
pro31: { model: "gemini-3.1-pro-preview", hint: "3.1 Pro — 플래그십 (1M ctx, 멀티모달)" },
|
|
9
|
+
flash3: { model: "gemini-3-flash-preview", hint: "3.0 Flash — 빠른 응답, 비용 효율" },
|
|
10
|
+
pro25: { model: "gemini-2.5-pro", hint: "2.5 Pro — 안정 (추론 강화)" },
|
|
11
|
+
flash25: { model: "gemini-2.5-flash", hint: "2.5 Flash — 경량 범용" },
|
|
12
|
+
lite25: { model: "gemini-2.5-flash-lite", hint: "2.5 Flash Lite — 최경량" },
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const DEFAULT_PROFILE_COUNT = Object.keys(DEFAULT_GEMINI_PROFILES.profiles).length;
|
|
17
|
+
|
|
18
|
+
function ensureGeminiProfiles({
|
|
19
|
+
geminiDir = join(homedir(), ".gemini"),
|
|
20
|
+
profilesPath = join(geminiDir, "triflux-profiles.json"),
|
|
21
|
+
} = {}) {
|
|
22
|
+
try {
|
|
23
|
+
if (!existsSync(geminiDir)) mkdirSync(geminiDir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
if (!existsSync(profilesPath)) {
|
|
26
|
+
writeFileSync(profilesPath, JSON.stringify(DEFAULT_GEMINI_PROFILES, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
27
|
+
return {
|
|
28
|
+
ok: true,
|
|
29
|
+
created: true,
|
|
30
|
+
added: DEFAULT_PROFILE_COUNT,
|
|
31
|
+
count: DEFAULT_PROFILE_COUNT,
|
|
32
|
+
message: null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let cfg;
|
|
37
|
+
try {
|
|
38
|
+
cfg = JSON.parse(readFileSync(profilesPath, "utf8"));
|
|
39
|
+
} catch {
|
|
40
|
+
try { copyFileSync(profilesPath, profilesPath + `.bak.${Date.now()}`); } catch {}
|
|
41
|
+
writeFileSync(profilesPath, JSON.stringify(DEFAULT_GEMINI_PROFILES, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
42
|
+
return {
|
|
43
|
+
ok: true,
|
|
44
|
+
created: true,
|
|
45
|
+
added: DEFAULT_PROFILE_COUNT,
|
|
46
|
+
count: DEFAULT_PROFILE_COUNT,
|
|
47
|
+
message: null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) cfg = {};
|
|
52
|
+
if (!cfg.profiles || typeof cfg.profiles !== "object" || Array.isArray(cfg.profiles)) cfg.profiles = {};
|
|
53
|
+
|
|
54
|
+
let added = 0;
|
|
55
|
+
for (const [name, value] of Object.entries(DEFAULT_GEMINI_PROFILES.profiles)) {
|
|
56
|
+
if (!cfg.profiles[name]) {
|
|
57
|
+
cfg.profiles[name] = value;
|
|
58
|
+
added++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!cfg.model) cfg.model = DEFAULT_GEMINI_PROFILES.model;
|
|
62
|
+
|
|
63
|
+
if (added > 0) {
|
|
64
|
+
writeFileSync(profilesPath, JSON.stringify(cfg, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
created: false,
|
|
70
|
+
added,
|
|
71
|
+
count: Object.keys(cfg.profiles).length,
|
|
72
|
+
message: null,
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
created: false,
|
|
78
|
+
added: 0,
|
|
79
|
+
count: 0,
|
|
80
|
+
message: error.message,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { DEFAULT_GEMINI_PROFILES, ensureGeminiProfiles };
|
package/scripts/setup.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { spawn, execFileSync } from "child_process";
|
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
12
12
|
import { cleanupTmpFiles } from "./tmp-cleanup.mjs";
|
|
13
13
|
import { buildAll as buildCacheWarmup } from "./cache-warmup.mjs";
|
|
14
|
+
import { ensureGeminiProfiles } from "./lib/gemini-profiles.mjs";
|
|
14
15
|
|
|
15
16
|
const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
16
17
|
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
@@ -220,6 +221,11 @@ const SYNC_MAP = [
|
|
|
220
221
|
dst: join(CLAUDE_DIR, "scripts", "lib", "keyword-rules.mjs"),
|
|
221
222
|
label: "lib/keyword-rules.mjs",
|
|
222
223
|
},
|
|
224
|
+
{
|
|
225
|
+
src: join(PLUGIN_ROOT, "scripts", "lib", "gemini-profiles.mjs"),
|
|
226
|
+
dst: join(CLAUDE_DIR, "scripts", "lib", "gemini-profiles.mjs"),
|
|
227
|
+
label: "lib/gemini-profiles.mjs",
|
|
228
|
+
},
|
|
223
229
|
{
|
|
224
230
|
src: join(PLUGIN_ROOT, "hub", "team", "agent-map.json"),
|
|
225
231
|
dst: join(CLAUDE_DIR, "hub", "team", "agent-map.json"),
|
|
@@ -331,56 +337,6 @@ function ensureCodexProfiles() {
|
|
|
331
337
|
}
|
|
332
338
|
}
|
|
333
339
|
|
|
334
|
-
const GEMINI_DIR = join(homedir(), ".gemini");
|
|
335
|
-
const GEMINI_PROFILES_PATH = join(GEMINI_DIR, "triflux-profiles.json");
|
|
336
|
-
|
|
337
|
-
const DEFAULT_GEMINI_PROFILES = {
|
|
338
|
-
model: "gemini-3.1-pro-preview",
|
|
339
|
-
profiles: {
|
|
340
|
-
pro31: { model: "gemini-3.1-pro-preview", hint: "3.1 Pro — 플래그십 (1M ctx, 멀티모달)" },
|
|
341
|
-
flash3: { model: "gemini-3-flash-preview", hint: "3.0 Flash — 빠른 응답, 비용 효율" },
|
|
342
|
-
pro25: { model: "gemini-2.5-pro", hint: "2.5 Pro — 안정 (추론 강화)" },
|
|
343
|
-
flash25: { model: "gemini-2.5-flash", hint: "2.5 Flash — 경량 범용" },
|
|
344
|
-
lite25: { model: "gemini-2.5-flash-lite", hint: "2.5 Flash Lite — 최경량" },
|
|
345
|
-
},
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
function ensureGeminiProfiles() {
|
|
349
|
-
try {
|
|
350
|
-
if (!existsSync(GEMINI_DIR)) mkdirSync(GEMINI_DIR, { recursive: true });
|
|
351
|
-
|
|
352
|
-
if (!existsSync(GEMINI_PROFILES_PATH)) {
|
|
353
|
-
writeFileSync(GEMINI_PROFILES_PATH, JSON.stringify(DEFAULT_GEMINI_PROFILES, null, 2) + "\n", "utf8");
|
|
354
|
-
return Object.keys(DEFAULT_GEMINI_PROFILES.profiles).length;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
let cfg;
|
|
358
|
-
try {
|
|
359
|
-
cfg = JSON.parse(readFileSync(GEMINI_PROFILES_PATH, "utf8"));
|
|
360
|
-
} catch {
|
|
361
|
-
writeFileSync(GEMINI_PROFILES_PATH, JSON.stringify(DEFAULT_GEMINI_PROFILES, null, 2) + "\n", "utf8");
|
|
362
|
-
return Object.keys(DEFAULT_GEMINI_PROFILES.profiles).length;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (!cfg.profiles) cfg.profiles = {};
|
|
366
|
-
let added = 0;
|
|
367
|
-
for (const [name, value] of Object.entries(DEFAULT_GEMINI_PROFILES.profiles)) {
|
|
368
|
-
if (!cfg.profiles[name]) {
|
|
369
|
-
cfg.profiles[name] = value;
|
|
370
|
-
added++;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
if (!cfg.model) cfg.model = DEFAULT_GEMINI_PROFILES.model;
|
|
374
|
-
|
|
375
|
-
if (added > 0) {
|
|
376
|
-
writeFileSync(GEMINI_PROFILES_PATH, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
377
|
-
}
|
|
378
|
-
return added;
|
|
379
|
-
} catch {
|
|
380
|
-
return 0;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
|
|
384
340
|
export { replaceProfileSection, hasProfileSection, detectDevMode, SYNC_MAP, BREADCRUMB_PATH, PLUGIN_ROOT, CLAUDE_DIR, SKILL_ALIASES };
|
|
385
341
|
|
|
386
342
|
async function main() {
|
|
@@ -865,7 +821,7 @@ if (codexProfilesAdded > 0) {
|
|
|
865
821
|
|
|
866
822
|
// ── Gemini 프로필 자동 보정 ──
|
|
867
823
|
|
|
868
|
-
const geminiProfilesAdded = ensureGeminiProfiles();
|
|
824
|
+
const geminiProfilesAdded = ensureGeminiProfiles().added;
|
|
869
825
|
if (geminiProfilesAdded > 0) {
|
|
870
826
|
synced++;
|
|
871
827
|
}
|
package/tui/setup.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
RESET, DIM, BOLD, CYAN, AMBER, GREEN, RED, YELLOW, WHITE, GRAY,
|
|
12
12
|
onExit, showCursor,
|
|
13
13
|
} from "./core.mjs";
|
|
14
|
+
import { DEFAULT_GEMINI_PROFILES } from "../scripts/lib/gemini-profiles.mjs";
|
|
14
15
|
|
|
15
16
|
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
16
17
|
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
@@ -101,20 +102,13 @@ function stepProfiles() {
|
|
|
101
102
|
function stepGeminiProfiles() {
|
|
102
103
|
const configPath = join(GEMINI_DIR, "triflux-profiles.json");
|
|
103
104
|
if (!existsSync(configPath)) {
|
|
104
|
-
// 기본 프로필 자동 생성
|
|
105
|
-
const defaultConfig = {
|
|
106
|
-
model: "gemini-3.1-pro-preview",
|
|
107
|
-
profiles: {
|
|
108
|
-
pro31: { model: "gemini-3.1-pro-preview", hint: "3.1 Pro — 플래그십 (1M ctx, 멀티모달)" },
|
|
109
|
-
flash3: { model: "gemini-3-flash-preview", hint: "3.0 Flash — 빠른 응답, 비용 효율" },
|
|
110
|
-
pro25: { model: "gemini-2.5-pro", hint: "2.5 Pro — 안정 (추론 강화)" },
|
|
111
|
-
flash25: { model: "gemini-2.5-flash", hint: "2.5 Flash — 경량 범용" },
|
|
112
|
-
lite25: { model: "gemini-2.5-flash-lite", hint: "2.5 Flash Lite — 최경량" },
|
|
113
|
-
},
|
|
114
|
-
};
|
|
115
105
|
if (!existsSync(GEMINI_DIR)) mkdirSync(GEMINI_DIR, { recursive: true });
|
|
116
|
-
writeFileSync(configPath, JSON.stringify(
|
|
117
|
-
return {
|
|
106
|
+
writeFileSync(configPath, JSON.stringify(DEFAULT_GEMINI_PROFILES, null, 2) + "\n", "utf8");
|
|
107
|
+
return {
|
|
108
|
+
ok: true,
|
|
109
|
+
detail: `기본 프로필 ${Object.keys(DEFAULT_GEMINI_PROFILES.profiles).length}개 자동 생성됨`,
|
|
110
|
+
action: "created",
|
|
111
|
+
};
|
|
118
112
|
}
|
|
119
113
|
|
|
120
114
|
try {
|
|
@@ -361,7 +355,7 @@ async function starRequest() {
|
|
|
361
355
|
return;
|
|
362
356
|
}
|
|
363
357
|
|
|
364
|
-
if (await confirm(`${AMBER}⭐${RESET} 하나가 큰 차이를 만듭니다.`,
|
|
358
|
+
if (await confirm(`${AMBER}⭐${RESET} 하나가 큰 차이를 만듭니다.`, false)) {
|
|
365
359
|
try {
|
|
366
360
|
execFileSync("gh", ["api", "-X", "PUT", "/user/starred/tellang/triflux"], {
|
|
367
361
|
timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -379,7 +373,7 @@ async function starRequest() {
|
|
|
379
373
|
// ── Main Menu ──
|
|
380
374
|
|
|
381
375
|
const MENU = [
|
|
382
|
-
{ label: "전체 설정 (Full Setup)", hint: "
|
|
376
|
+
{ label: "전체 설정 (Full Setup)", hint: "6단계 순서 실행" },
|
|
383
377
|
{ label: "단계별 선택 (Selective)", hint: "특정 단계만 실행" },
|
|
384
378
|
{ label: "현재 상태 확인 (Status)", hint: "설정 없이 진단만" },
|
|
385
379
|
{ label: "종료", hint: "Ctrl+C" },
|