triflux 9.1.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 +83 -9
- 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 +13 -0
- package/skills/star-prompt/SKILL.md +122 -0
- package/tui/setup.mjs +59 -14
package/bin/triflux.mjs
CHANGED
|
@@ -11,6 +11,7 @@ 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");
|
|
@@ -18,6 +19,9 @@ const CODEX_DIR = join(homedir(), ".codex");
|
|
|
18
19
|
const CODEX_CONFIG_PATH = join(CODEX_DIR, "config.toml");
|
|
19
20
|
const PKG = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
20
21
|
|
|
22
|
+
// 이 배열에 포함된 버전에서만 star prompt를 표시한다 (빈 배열 = 모든 버전에서 표시)
|
|
23
|
+
const STAR_PROMPT_VERSIONS = ["9.2.1"];
|
|
24
|
+
|
|
21
25
|
const REQUIRED_CODEX_PROFILES = [
|
|
22
26
|
{
|
|
23
27
|
name: "codex53_high",
|
|
@@ -662,6 +666,11 @@ function getSetupSyncTargets() {
|
|
|
662
666
|
dst: join(CLAUDE_DIR, "scripts", "lib", "keyword-rules.mjs"),
|
|
663
667
|
label: "lib/keyword-rules.mjs",
|
|
664
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
|
+
},
|
|
665
674
|
{
|
|
666
675
|
src: join(PKG_ROOT, "scripts", "tfx-route-worker.mjs"),
|
|
667
676
|
dst: join(CLAUDE_DIR, "scripts", "tfx-route-worker.mjs"),
|
|
@@ -816,7 +825,7 @@ function buildSetupDryRunPlan() {
|
|
|
816
825
|
}
|
|
817
826
|
|
|
818
827
|
function cmdSetup(options = {}) {
|
|
819
|
-
const { dryRun = false } = options;
|
|
828
|
+
const { dryRun = false, fromUpdate = false } = options;
|
|
820
829
|
if (dryRun) {
|
|
821
830
|
printJson(buildSetupDryRunPlan());
|
|
822
831
|
return;
|
|
@@ -869,19 +878,42 @@ function cmdSetup(options = {}) {
|
|
|
869
878
|
}
|
|
870
879
|
}
|
|
871
880
|
|
|
881
|
+
// ── 결과 추적 ──
|
|
882
|
+
const summary = [];
|
|
883
|
+
|
|
872
884
|
const codexProfileResult = ensureCodexProfiles();
|
|
873
885
|
if (!codexProfileResult.ok) {
|
|
874
886
|
warn(`Codex profiles 설정 실패: ${codexProfileResult.message}`);
|
|
887
|
+
summary.push({ item: "Codex profiles", status: "⚠️", detail: codexProfileResult.message });
|
|
875
888
|
} else if (codexProfileResult.added > 0) {
|
|
876
889
|
ok(`Codex profiles: ${codexProfileResult.added}개 추가됨 (~/.codex/config.toml)`);
|
|
890
|
+
summary.push({ item: "Codex profiles", status: "✅", detail: `${codexProfileResult.added}개 추가됨` });
|
|
877
891
|
} else {
|
|
878
892
|
ok("Codex profiles: 이미 준비됨");
|
|
893
|
+
summary.push({ item: "Codex profiles", status: "✅", detail: "이미 준비됨" });
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// Gemini 프로필
|
|
897
|
+
const geminiResult = ensureGeminiProfiles();
|
|
898
|
+
if (!geminiResult.ok) {
|
|
899
|
+
warn(`Gemini profiles 설정 실패: ${geminiResult.message}`);
|
|
900
|
+
summary.push({ item: "Gemini profiles", status: "⚠️", detail: geminiResult.message });
|
|
901
|
+
} else if (geminiResult.created) {
|
|
902
|
+
ok(`Gemini profiles: ${geminiResult.count}개 생성됨 (~/.gemini/triflux-profiles.json)`);
|
|
903
|
+
summary.push({ item: "Gemini profiles", status: "✅", detail: `${geminiResult.count}개 생성됨` });
|
|
904
|
+
} else if (geminiResult.added > 0) {
|
|
905
|
+
ok(`Gemini profiles: ${geminiResult.added}개 추가됨`);
|
|
906
|
+
summary.push({ item: "Gemini profiles", status: "✅", detail: `${geminiResult.added}개 추가됨 (총 ${geminiResult.count}개)` });
|
|
907
|
+
} else {
|
|
908
|
+
ok(`Gemini profiles: ${geminiResult.count}개 준비됨`);
|
|
909
|
+
summary.push({ item: "Gemini profiles", status: "✅", detail: `${geminiResult.count}개 준비됨` });
|
|
879
910
|
}
|
|
880
911
|
|
|
881
912
|
// hub MCP 사전 등록 (서버 미실행이어도 설정만 등록 — hub start 시 즉시 사용 가능)
|
|
882
913
|
if (existsSync(join(PKG_ROOT, "hub", "server.mjs"))) {
|
|
883
914
|
const defaultHubUrl = `http://127.0.0.1:${process.env.TFX_HUB_PORT || "27888"}/mcp`;
|
|
884
915
|
autoRegisterMcp(defaultHubUrl);
|
|
916
|
+
summary.push({ item: "Hub MCP", status: "✅", detail: "등록됨" });
|
|
885
917
|
console.log("");
|
|
886
918
|
}
|
|
887
919
|
|
|
@@ -900,6 +932,7 @@ function cmdSetup(options = {}) {
|
|
|
900
932
|
const currentCmd = settings.statusLine?.command || "";
|
|
901
933
|
if (currentCmd.includes("hud-qos-status.mjs")) {
|
|
902
934
|
ok("statusLine 이미 설정됨");
|
|
935
|
+
summary.push({ item: "HUD statusLine", status: "✅", detail: "이미 설정됨" });
|
|
903
936
|
} else {
|
|
904
937
|
const nodePath = process.execPath.replace(/\\/g, "/");
|
|
905
938
|
const hudForward = hudPath.replace(/\\/g, "/");
|
|
@@ -917,6 +950,7 @@ function cmdSetup(options = {}) {
|
|
|
917
950
|
|
|
918
951
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
919
952
|
ok("statusLine 설정 완료 — 세션 재시작 후 HUD 표시");
|
|
953
|
+
summary.push({ item: "HUD statusLine", status: "✅", detail: "설정 완료" });
|
|
920
954
|
}
|
|
921
955
|
} catch (e) {
|
|
922
956
|
throw createCliError(`settings.json 처리 실패: ${e.message}`, {
|
|
@@ -928,9 +962,50 @@ function cmdSetup(options = {}) {
|
|
|
928
962
|
}
|
|
929
963
|
} else {
|
|
930
964
|
warn("HUD 파일 없음 — 먼저 파일 동기화 필요");
|
|
965
|
+
summary.push({ item: "HUD statusLine", status: "⚠️", detail: "HUD 파일 없음" });
|
|
931
966
|
}
|
|
932
967
|
|
|
933
|
-
|
|
968
|
+
// CLI 존재 확인
|
|
969
|
+
const cliChecks = [
|
|
970
|
+
{ name: "codex", install: "npm i -g @openai/codex" },
|
|
971
|
+
{ name: "gemini", install: "npm i -g @google/gemini-cli" },
|
|
972
|
+
];
|
|
973
|
+
for (const { name, install } of cliChecks) {
|
|
974
|
+
if (which(name)) {
|
|
975
|
+
summary.push({ item: `${name} CLI`, status: "✅", detail: "설치됨" });
|
|
976
|
+
} else {
|
|
977
|
+
summary.push({ item: `${name} CLI`, status: "⏭️", detail: `미설치 (${install})` });
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Star request (버전 게이팅)
|
|
982
|
+
const showStar = !fromUpdate && (STAR_PROMPT_VERSIONS.length === 0 || STAR_PROMPT_VERSIONS.includes(PKG.version));
|
|
983
|
+
if (showStar) {
|
|
984
|
+
try {
|
|
985
|
+
execFileSync("gh", ["auth", "status"], { timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
986
|
+
try {
|
|
987
|
+
execFileSync("gh", ["api", "user/starred/tellang/triflux"], { timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
988
|
+
console.log();
|
|
989
|
+
ok(`이미 함께하고 계시군요. ${AMBER}⭐${RESET}`);
|
|
990
|
+
} catch {
|
|
991
|
+
console.log();
|
|
992
|
+
info(`${AMBER}⭐${RESET} 하나가 큰 차이를 만듭니다. ${CYAN}https://github.com/tellang/triflux${RESET}`);
|
|
993
|
+
}
|
|
994
|
+
} catch {
|
|
995
|
+
console.log();
|
|
996
|
+
info(`${AMBER}⭐${RESET} 하나가 큰 차이를 만듭니다. ${CYAN}https://github.com/tellang/triflux${RESET}`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// ── 결과 요약 테이블 ──
|
|
1001
|
+
console.log(`\n${BOLD}── 설정 요약 ──${RESET}`);
|
|
1002
|
+
const maxItem = Math.max(...summary.map((s) => s.item.length));
|
|
1003
|
+
for (const { item, status, detail } of summary) {
|
|
1004
|
+
console.log(` ${status} ${item.padEnd(maxItem)} ${DIM}${detail}${RESET}`);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
console.log(`\n${DIM}설치 위치: ${CLAUDE_DIR}${RESET}`);
|
|
1008
|
+
console.log(`${DIM}버전: v${PKG.version}${RESET}\n`);
|
|
934
1009
|
}
|
|
935
1010
|
|
|
936
1011
|
function addDoctorCheck(report, entry) {
|
|
@@ -1849,7 +1924,7 @@ function cmdUpdate() {
|
|
|
1849
1924
|
return;
|
|
1850
1925
|
}
|
|
1851
1926
|
|
|
1852
|
-
// 3. setup 재실행 (
|
|
1927
|
+
// 3. setup 재실행 (파일 동기화, 프로파일, HUD, CLI 확인)
|
|
1853
1928
|
if (updated) {
|
|
1854
1929
|
console.log("");
|
|
1855
1930
|
// 업데이트 후 새 버전 읽기
|
|
@@ -1865,18 +1940,17 @@ function cmdUpdate() {
|
|
|
1865
1940
|
ok(`버전: v${oldVer} (이미 최신)`);
|
|
1866
1941
|
}
|
|
1867
1942
|
|
|
1868
|
-
// setup 재실행
|
|
1869
|
-
console.log(
|
|
1870
|
-
|
|
1871
|
-
cmdSetup();
|
|
1943
|
+
// setup 재실행 — 개선된 cmdSetup()이 Gemini 프로필, CLI 확인, 요약 테이블 포함
|
|
1944
|
+
console.log(`\n${CYAN}── 설정 동기화 ──${RESET}`);
|
|
1945
|
+
cmdSetup({ fromUpdate: true });
|
|
1872
1946
|
|
|
1873
1947
|
if (stoppedHubInfo) {
|
|
1874
|
-
if (startHubAfterUpdate(stoppedHubInfo))
|
|
1948
|
+
if (startHubAfterUpdate(stoppedHubInfo)) ok("hub 재기동 완료");
|
|
1875
1949
|
else warn("hub 재기동 실패 — `tfx hub start`로 수동 시작 필요");
|
|
1876
1950
|
}
|
|
1877
1951
|
}
|
|
1878
1952
|
|
|
1879
|
-
console.log(`${GREEN}${BOLD}업데이트 완료${RESET}\n`);
|
|
1953
|
+
console.log(`${GREEN}${BOLD}✓ 업데이트 완료${RESET}\n`);
|
|
1880
1954
|
}
|
|
1881
1955
|
|
|
1882
1956
|
function cmdList(options = {}) {
|
|
@@ -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"),
|
|
@@ -813,6 +819,13 @@ if (codexProfilesAdded > 0) {
|
|
|
813
819
|
synced++;
|
|
814
820
|
}
|
|
815
821
|
|
|
822
|
+
// ── Gemini 프로필 자동 보정 ──
|
|
823
|
+
|
|
824
|
+
const geminiProfilesAdded = ensureGeminiProfiles().added;
|
|
825
|
+
if (geminiProfilesAdded > 0) {
|
|
826
|
+
synced++;
|
|
827
|
+
}
|
|
828
|
+
|
|
816
829
|
// ── MCP 인벤토리 백그라운드 갱신 ──
|
|
817
830
|
|
|
818
831
|
const mcpCheck = join(PLUGIN_ROOT, "scripts", "mcp-check.mjs");
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: star-prompt
|
|
3
|
+
description: >-
|
|
4
|
+
CLI 프로젝트의 setup/postinstall 흐름에 GitHub 스타 요청 프롬프트를 추가한다.
|
|
5
|
+
gh CLI 인증 확인 → 이미 스타 여부 감지 → 인터랙티브 confirm → gh API로 자동 스타.
|
|
6
|
+
Apple 스타일 UX 카피 포함. 'star prompt', '스타 요청', '리포 스타', 'star request',
|
|
7
|
+
'깃헙 스타 넣어줘', 'star 눌러달라고', '응원 요청' 같은 요청에 사용한다.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# tfx-star-prompt — GitHub Star Request Prompt
|
|
11
|
+
|
|
12
|
+
CLI 도구의 setup 완료 시점에 GitHub 리포 스타를 요청하는 프롬프트를 추가한다.
|
|
13
|
+
사용자 경험을 해치지 않으면서, 이미 스타한 사용자에겐 감사를 표하고, 아직 안 한 사용자에겐 부담 없이 한 번 물어본다.
|
|
14
|
+
|
|
15
|
+
## 동작 흐름
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
gh auth status ─── 실패 → URL만 표시 (비인터랙티브 폴백)
|
|
19
|
+
│
|
|
20
|
+
✓ 인증됨
|
|
21
|
+
│
|
|
22
|
+
gh api user/starred/{owner}/{repo} ─── 성공 → "이미 함께하고 계시군요. ⭐"
|
|
23
|
+
│
|
|
24
|
+
✗ 미스타
|
|
25
|
+
│
|
|
26
|
+
confirm("⭐ 하나가 큰 차이를 만듭니다.") ─── N → URL 조용히 표시
|
|
27
|
+
│
|
|
28
|
+
Y
|
|
29
|
+
│
|
|
30
|
+
gh api -X PUT /user/starred/{owner}/{repo} → "함께해 주셔서 감사합니다. ⭐"
|
|
31
|
+
│
|
|
32
|
+
실패 → URL 폴백
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 구현 패턴
|
|
36
|
+
|
|
37
|
+
### 인터랙티브 (TUI / readline confirm)
|
|
38
|
+
|
|
39
|
+
setup 위저드 완료 후 호출. `confirm()` 으로 Y/n 입력 받고 `gh api`로 자동 스타.
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
async function starRequest() {
|
|
43
|
+
let ghOk = false;
|
|
44
|
+
try {
|
|
45
|
+
execFileSync("gh", ["auth", "status"], {
|
|
46
|
+
timeout: 5000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
|
|
47
|
+
});
|
|
48
|
+
ghOk = true;
|
|
49
|
+
} catch {}
|
|
50
|
+
|
|
51
|
+
if (!ghOk) {
|
|
52
|
+
// gh 미설치/미인증 — URL만 표시
|
|
53
|
+
info(`⭐ 하나가 큰 차이를 만듭니다. https://github.com/{owner}/{repo}`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let alreadyStarred = false;
|
|
58
|
+
try {
|
|
59
|
+
execFileSync("gh", ["api", "user/starred/{owner}/{repo}"], {
|
|
60
|
+
timeout: 5000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
|
|
61
|
+
});
|
|
62
|
+
alreadyStarred = true;
|
|
63
|
+
} catch {}
|
|
64
|
+
|
|
65
|
+
if (alreadyStarred) {
|
|
66
|
+
ok(`이미 함께하고 계시군요. ⭐`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (await confirm(`⭐ 하나가 큰 차이를 만듭니다.`, true)) {
|
|
71
|
+
try {
|
|
72
|
+
execFileSync("gh", ["api", "-X", "PUT", "/user/starred/{owner}/{repo}"], {
|
|
73
|
+
timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
|
|
74
|
+
});
|
|
75
|
+
ok(`함께해 주셔서 감사합니다. ⭐`);
|
|
76
|
+
} catch {
|
|
77
|
+
info(`https://github.com/{owner}/{repo}`);
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
// 거절 시 URL만 조용히
|
|
81
|
+
console.log(` https://github.com/{owner}/{repo}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 비인터랙티브 (postinstall / CLI banner)
|
|
87
|
+
|
|
88
|
+
confirm 불가한 환경. 이미 스타 여부만 감지하고 메시지 분기.
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
try {
|
|
92
|
+
execFileSync("gh", ["auth", "status"], { timeout: 5000, stdio: ["pipe","pipe","pipe"] });
|
|
93
|
+
try {
|
|
94
|
+
execFileSync("gh", ["api", "user/starred/{owner}/{repo}"], { timeout: 5000, stdio: ["pipe","pipe","pipe"] });
|
|
95
|
+
ok(`이미 함께하고 계시군요. ⭐`);
|
|
96
|
+
} catch {
|
|
97
|
+
info(`⭐ 하나가 큰 차이를 만듭니다. https://github.com/{owner}/{repo}`);
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
info(`⭐ 하나가 큰 차이를 만듭니다. https://github.com/{owner}/{repo}`);
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 적용 시 규칙
|
|
105
|
+
|
|
106
|
+
1. `{owner}/{repo}`를 대상 리포로 치환한다
|
|
107
|
+
2. 프로젝트의 기존 ANSI 컬러 상수(AMBER, CYAN, RESET 등)를 사용한다
|
|
108
|
+
3. setup 완료 직후, 최종 요약 다음에 호출한다 — 핵심 설정 흐름을 방해하지 않는다
|
|
109
|
+
4. 모든 gh 호출은 `timeout: 5000`, `stdio: ["pipe","pipe","pipe"]`로 감싸서 실패해도 setup을 블로킹하지 않는다
|
|
110
|
+
5. 거절 시 죄책감을 주지 않는다 — URL만 조용히 남긴다
|
|
111
|
+
|
|
112
|
+
## UX 카피 톤
|
|
113
|
+
|
|
114
|
+
Apple 스타일: 짧고, 자신감 있고, 부담 없이.
|
|
115
|
+
|
|
116
|
+
| 상황 | 멘트 |
|
|
117
|
+
|------|------|
|
|
118
|
+
| 이미 스타 | `이미 함께하고 계시군요. ⭐` |
|
|
119
|
+
| 요청 | `⭐ 하나가 큰 차이를 만듭니다.` |
|
|
120
|
+
| 수락 후 | `함께해 주셔서 감사합니다. ⭐` |
|
|
121
|
+
| 거절 | URL만 |
|
|
122
|
+
| gh 없음 | `⭐ 하나가 큰 차이를 만듭니다.` + URL |
|
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 {
|
|
@@ -326,10 +320,60 @@ function showSummary(results) {
|
|
|
326
320
|
}
|
|
327
321
|
}
|
|
328
322
|
|
|
323
|
+
// ── Star Request ──
|
|
324
|
+
|
|
325
|
+
async function starRequest() {
|
|
326
|
+
let ghOk = false;
|
|
327
|
+
try {
|
|
328
|
+
execFileSync("gh", ["auth", "status"], {
|
|
329
|
+
timeout: 5000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
|
|
330
|
+
});
|
|
331
|
+
ghOk = true;
|
|
332
|
+
} catch {}
|
|
333
|
+
|
|
334
|
+
if (!ghOk) {
|
|
335
|
+
console.log();
|
|
336
|
+
info(`${AMBER}⭐${RESET} 하나가 큰 차이를 만듭니다. ${CYAN}https://github.com/tellang/triflux${RESET}`);
|
|
337
|
+
console.log();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// gh 인증됨 — 스타 여부 확인
|
|
342
|
+
let alreadyStarred = false;
|
|
343
|
+
try {
|
|
344
|
+
execFileSync("gh", ["api", "user/starred/tellang/triflux"], {
|
|
345
|
+
timeout: 5000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
|
|
346
|
+
});
|
|
347
|
+
alreadyStarred = true;
|
|
348
|
+
} catch {}
|
|
349
|
+
|
|
350
|
+
console.log();
|
|
351
|
+
|
|
352
|
+
if (alreadyStarred) {
|
|
353
|
+
ok(`이미 함께하고 계시군요. ${AMBER}⭐${RESET}`);
|
|
354
|
+
console.log();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (await confirm(`${AMBER}⭐${RESET} 하나가 큰 차이를 만듭니다.`, false)) {
|
|
359
|
+
try {
|
|
360
|
+
execFileSync("gh", ["api", "-X", "PUT", "/user/starred/tellang/triflux"], {
|
|
361
|
+
timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
|
|
362
|
+
});
|
|
363
|
+
ok(`함께해 주셔서 감사합니다. ${AMBER}⭐${RESET}`);
|
|
364
|
+
} catch {
|
|
365
|
+
info(`${CYAN}https://github.com/tellang/triflux${RESET}`);
|
|
366
|
+
}
|
|
367
|
+
} else {
|
|
368
|
+
console.log(` ${DIM}${CYAN}https://github.com/tellang/triflux${RESET}`);
|
|
369
|
+
}
|
|
370
|
+
console.log();
|
|
371
|
+
}
|
|
372
|
+
|
|
329
373
|
// ── Main Menu ──
|
|
330
374
|
|
|
331
375
|
const MENU = [
|
|
332
|
-
{ label: "전체 설정 (Full Setup)", hint: "
|
|
376
|
+
{ label: "전체 설정 (Full Setup)", hint: "6단계 순서 실행" },
|
|
333
377
|
{ label: "단계별 선택 (Selective)", hint: "특정 단계만 실행" },
|
|
334
378
|
{ label: "현재 상태 확인 (Status)", hint: "설정 없이 진단만" },
|
|
335
379
|
{ label: "종료", hint: "Ctrl+C" },
|
|
@@ -357,6 +401,7 @@ async function main() {
|
|
|
357
401
|
case 0: {
|
|
358
402
|
const results = await runWizard();
|
|
359
403
|
showSummary(results);
|
|
404
|
+
await starRequest();
|
|
360
405
|
break;
|
|
361
406
|
}
|
|
362
407
|
|