triflux 10.9.21 → 10.9.22

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.
Files changed (99) hide show
  1. package/.claude-plugin/marketplace.json +34 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/config/mcp-registry.json +29 -0
  4. package/hub/account-broker.mjs +6 -4
  5. package/hub/cli-adapter-base.mjs +14 -14
  6. package/hub/lib/env-detect.mjs +47 -20
  7. package/hub/server.mjs +17 -15
  8. package/hub/team/headless.mjs +10 -0
  9. package/hub/team/swarm-hypervisor.mjs +2 -2
  10. package/hud/constants.mjs +24 -13
  11. package/hud/renderers.mjs +2 -1
  12. package/package.json +62 -21
  13. package/scripts/__tests__/keyword-detector.test.mjs +4 -4
  14. package/scripts/__tests__/release-governance.test.mjs +148 -0
  15. package/scripts/doctor-diagnose.mjs +6 -7
  16. package/scripts/lib/cross-review-utils.mjs +2 -2
  17. package/scripts/lib/mcp-filter.mjs +9 -5
  18. package/scripts/release/bump-version.mjs +77 -0
  19. package/scripts/release/check-sync.mjs +51 -0
  20. package/scripts/release/lib.mjs +303 -0
  21. package/scripts/release/prepare.mjs +85 -0
  22. package/scripts/release/publish.mjs +87 -0
  23. package/scripts/release/verify.mjs +81 -0
  24. package/scripts/release/version-manifest.json +26 -0
  25. package/scripts/remote-spawn.mjs +3 -3
  26. package/scripts/setup.mjs +18 -15
  27. package/scripts/tfx-route.sh +64 -8
  28. package/tui/codex-profile.mjs +457 -0
  29. package/tui/core.mjs +266 -0
  30. package/tui/doctor.mjs +375 -0
  31. package/tui/gemini-profile.mjs +299 -0
  32. package/tui/monitor-data.mjs +152 -0
  33. package/tui/monitor.mjs +339 -0
  34. package/tui/setup.mjs +598 -0
  35. package/CLAUDE.md +0 -212
  36. package/references/hosts.json +0 -46
  37. package/skills/tfx-workspace/async-tests/run-tests.sh +0 -203
  38. package/skills/tfx-workspace/evals/evals.json +0 -79
  39. package/skills/tfx-workspace/iteration-1/benchmark.json +0 -524
  40. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +0 -11
  41. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +0 -25
  42. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +0 -154
  43. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +0 -5
  44. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +0 -25
  45. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +0 -126
  46. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +0 -5
  47. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +0 -11
  48. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +0 -25
  49. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +0 -119
  50. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +0 -5
  51. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +0 -25
  52. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +0 -115
  53. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +0 -5
  54. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +0 -10
  55. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +0 -20
  56. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +0 -86
  57. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +0 -5
  58. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +0 -20
  59. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +0 -81
  60. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +0 -5
  61. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +0 -12
  62. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +0 -30
  63. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +0 -316
  64. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +0 -5
  65. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +0 -30
  66. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +0 -352
  67. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +0 -5
  68. package/skills/tfx-workspace/iteration-1/review.html +0 -1325
  69. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +0 -12
  70. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +0 -30
  71. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +0 -97
  72. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +0 -5
  73. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +0 -30
  74. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +0 -94
  75. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +0 -5
  76. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +0 -12
  77. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +0 -30
  78. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +0 -209
  79. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +0 -5
  80. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +0 -30
  81. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +0 -193
  82. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +0 -5
  83. package/skills/tfx-workspace/iteration-2/benchmark.json +0 -144
  84. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +0 -13
  85. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +0 -35
  86. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +0 -382
  87. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +0 -5
  88. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +0 -35
  89. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +0 -333
  90. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +0 -5
  91. package/skills/tfx-workspace/iteration-2/review.html +0 -1325
  92. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +0 -217
  93. package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +0 -77
  94. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +0 -65
  95. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +0 -94
  96. package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +0 -82
  97. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +0 -133
  98. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +0 -426
  99. package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +0 -101
package/tui/doctor.mjs ADDED
@@ -0,0 +1,375 @@
1
+ #!/usr/bin/env node
2
+ // tui/doctor.mjs — Interactive triflux doctor TUI
3
+ import { execFileSync } from "node:child_process";
4
+ import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import {
9
+ BOLD,
10
+ box,
11
+ clear,
12
+ confirm,
13
+ DIM,
14
+ divider,
15
+ fail,
16
+ GRAY,
17
+ GREEN,
18
+ info,
19
+ label,
20
+ ok,
21
+ onExit,
22
+ RED,
23
+ RESET,
24
+ select,
25
+ showCursor,
26
+ spinner,
27
+ table,
28
+ warn,
29
+ YELLOW,
30
+ } from "./core.mjs";
31
+
32
+ const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
33
+ const CLAUDE_DIR = join(homedir(), ".claude");
34
+ const CACHE_DIR = join(CLAUDE_DIR, "cache");
35
+
36
+ const CACHE_FILES = [
37
+ { name: "claude-usage-cache.json", desc: "Claude 사용량" },
38
+ { name: ".claude-refresh-lock", desc: "리프레시 락" },
39
+ { name: "codex-rate-limits-cache.json", desc: "Codex 레이트 리밋" },
40
+ { name: "gemini-quota-cache.json", desc: "Gemini 쿼터" },
41
+ { name: "gemini-project-id.json", desc: "Gemini 프로젝트 ID" },
42
+ { name: "gemini-session-cache.json", desc: "Gemini 세션" },
43
+ { name: "gemini-rpm-tracker.json", desc: "Gemini RPM" },
44
+ { name: "sv-accumulator.json", desc: "절약량 누적" },
45
+ { name: "mcp-inventory.json", desc: "MCP 인벤토리" },
46
+ { name: "cli-issues.jsonl", desc: "CLI 이슈 로그" },
47
+ { name: "triflux-update-check.json", desc: "업데이트 체크" },
48
+ { name: "tfx-preflight.json", desc: "Preflight 캐시 (CLI/Hub 가용성)" },
49
+ ];
50
+
51
+ // ── Run triflux doctor --json ──
52
+
53
+ function runDoctor(mode = "check") {
54
+ const args = [join(PKG_ROOT, "bin", "triflux.mjs"), "doctor", "--json"];
55
+ if (mode === "fix") args.push("--fix");
56
+
57
+ try {
58
+ const out = execFileSync(process.execPath, args, {
59
+ timeout: 30000,
60
+ encoding: "utf8",
61
+ windowsHide: true,
62
+ });
63
+ // Extract JSON from output (may have ANSI/text before it)
64
+ const jsonMatch = out.match(/\{[\s\S]*\}$/m);
65
+ if (jsonMatch) return JSON.parse(jsonMatch[0]);
66
+ } catch (e) {
67
+ if (e.stdout) {
68
+ const jsonMatch = e.stdout.match(/\{[\s\S]*\}$/m);
69
+ if (jsonMatch) return JSON.parse(jsonMatch[0]);
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+
75
+ // ── Display Results ──
76
+
77
+ function statusIcon(status) {
78
+ const map = {
79
+ ok: `${GREEN}✓${RESET}`,
80
+ missing: `${RED}✗${RESET}`,
81
+ partial: `${YELLOW}⚠${RESET}`,
82
+ warning: `${YELLOW}⚠${RESET}`,
83
+ optional_missing: `${GRAY}○${RESET}`,
84
+ };
85
+ return map[status] || `${GRAY}?${RESET}`;
86
+ }
87
+
88
+ function showReport(report) {
89
+ if (!report) {
90
+ fail("진단 결과를 가져올 수 없습니다.");
91
+ return;
92
+ }
93
+
94
+ console.log();
95
+ const statusColor =
96
+ report.issue_count === 0 ? GREEN : report.issue_count <= 2 ? YELLOW : RED;
97
+ label(
98
+ "상태",
99
+ `${statusColor}${report.issue_count === 0 ? "정상" : `${report.issue_count}개 이슈`}${RESET}`,
100
+ );
101
+ label("모드", report.mode);
102
+ console.log();
103
+
104
+ const headers = ["항목", "상태", "비고"];
105
+ const rows = (report.checks || []).map((c) => {
106
+ let note = "";
107
+ if (c.version) note = `v${c.version}`;
108
+ if (c.missing_profiles?.length)
109
+ note = `누락: ${c.missing_profiles.join(", ")}`;
110
+ if (c.fix) note += note ? ` → ${c.fix}` : c.fix;
111
+ if (c.path && !c.fix) note = c.path;
112
+
113
+ const icon =
114
+ c.status === "ok"
115
+ ? statusIcon("ok")
116
+ : c.optional
117
+ ? statusIcon("optional_missing")
118
+ : statusIcon(c.status);
119
+
120
+ return [
121
+ `${icon} ${c.name}`,
122
+ c.status === "ok" ? `${GREEN}정상${RESET}` : `${RED}${c.status}${RESET}`,
123
+ note ? `${DIM}${note}${RESET}` : "",
124
+ ];
125
+ });
126
+
127
+ if (rows.length > 0) table(headers, rows);
128
+
129
+ // Actions (from fix/reset mode)
130
+ if (report.actions?.length > 0) {
131
+ console.log();
132
+ info(`수행된 작업: ${report.actions.length}개`);
133
+ for (const action of report.actions) {
134
+ const icon =
135
+ action.status === "ok" ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;
136
+ console.log(
137
+ ` ${icon} ${action.type}: ${action.name || action.path || ""}`,
138
+ );
139
+ }
140
+ }
141
+ }
142
+
143
+ // ── Cache Management ──
144
+
145
+ function getCacheStatus() {
146
+ const results = [];
147
+ for (const { name, desc } of CACHE_FILES) {
148
+ const fp = join(CACHE_DIR, name);
149
+ if (existsSync(fp)) {
150
+ let size = 0;
151
+ try {
152
+ size = readFileSync(fp).length;
153
+ } catch {}
154
+ let hasError = false;
155
+ try {
156
+ const parsed = JSON.parse(readFileSync(fp, "utf8"));
157
+ hasError = !!parsed.error;
158
+ } catch {
159
+ hasError = true;
160
+ }
161
+ results.push({ name, desc, exists: true, size, hasError });
162
+ } else {
163
+ results.push({ name, desc, exists: false, size: 0, hasError: false });
164
+ }
165
+ }
166
+ return results;
167
+ }
168
+
169
+ function showCacheStatus() {
170
+ const caches = getCacheStatus();
171
+ const existing = caches.filter((c) => c.exists);
172
+
173
+ if (existing.length === 0) {
174
+ info("캐시 파일 없음 (깨끗한 상태)");
175
+ return;
176
+ }
177
+
178
+ console.log();
179
+ const headers = ["캐시", "크기", "상태"];
180
+ const rows = existing.map((c) => [
181
+ c.desc,
182
+ c.size < 1024 ? `${c.size}B` : `${(c.size / 1024).toFixed(1)}KB`,
183
+ c.hasError ? `${RED}에러${RESET}` : `${GREEN}정상${RESET}`,
184
+ ]);
185
+ table(headers, rows);
186
+ }
187
+
188
+ async function selectiveReset() {
189
+ const caches = getCacheStatus().filter((c) => c.exists);
190
+ if (caches.length === 0) {
191
+ info("삭제할 캐시 파일이 없습니다.");
192
+ return;
193
+ }
194
+
195
+ const options = [
196
+ { label: "전체 삭제", hint: `${caches.length}개 파일` },
197
+ { label: "에러 캐시만 삭제", hint: "손상된 파일만" },
198
+ { label: "선택 삭제", hint: "하나씩 선택" },
199
+ { label: "취소", hint: "" },
200
+ ];
201
+
202
+ const choice = await select("삭제 방식", options);
203
+ if (!choice || choice.index === 3) return;
204
+
205
+ let targets = [];
206
+ if (choice.index === 0) {
207
+ targets = caches;
208
+ } else if (choice.index === 1) {
209
+ targets = caches.filter((c) => c.hasError);
210
+ if (targets.length === 0) {
211
+ info("에러 상태의 캐시가 없습니다.");
212
+ return;
213
+ }
214
+ } else {
215
+ for (const c of caches) {
216
+ const del = await confirm(`${c.desc} (${c.name}) 삭제?`, c.hasError);
217
+ if (del) targets.push(c);
218
+ }
219
+ }
220
+
221
+ if (targets.length === 0) return;
222
+
223
+ if (!(await confirm(`${targets.length}개 캐시 파일을 삭제하시겠습니까?`)))
224
+ return;
225
+
226
+ let deleted = 0;
227
+ for (const c of targets) {
228
+ try {
229
+ unlinkSync(join(CACHE_DIR, c.name));
230
+ ok(`삭제: ${c.desc}`);
231
+ deleted++;
232
+ } catch (e) {
233
+ fail(`삭제 실패: ${c.desc} — ${e.message}`);
234
+ }
235
+ }
236
+
237
+ ok(`${BOLD}${deleted}개${RESET} 캐시 파일 삭제 완료`);
238
+ }
239
+
240
+ // ── Orphan Teams ──
241
+
242
+ async function checkOrphanTeams() {
243
+ const teamsDir = join(CLAUDE_DIR, "teams");
244
+ if (!existsSync(teamsDir)) {
245
+ info("teams 디렉토리 없음");
246
+ return;
247
+ }
248
+
249
+ const entries = readdirSync(teamsDir).filter((e) => !e.startsWith("."));
250
+ if (entries.length === 0) {
251
+ ok("잔존 팀 없음");
252
+ return;
253
+ }
254
+
255
+ warn(`${entries.length}개 팀 세션 발견`);
256
+ for (const e of entries) {
257
+ console.log(` ${DIM}${e}${RESET}`);
258
+ }
259
+
260
+ if (await confirm("잔존 팀 정리를 시도하시겠습니까?", false)) {
261
+ const spin = spinner("팀 정리 중...");
262
+ try {
263
+ // Delegate to triflux's cleanup
264
+ execFileSync(
265
+ process.execPath,
266
+ [join(PKG_ROOT, "bin", "triflux.mjs"), "doctor", "--fix"],
267
+ {
268
+ timeout: 30000,
269
+ stdio: "ignore",
270
+ windowsHide: true,
271
+ },
272
+ );
273
+ spin.stop();
274
+ ok("팀 정리 완료");
275
+ } catch {
276
+ spin.stop();
277
+ warn("팀 정리 실패 — 수동 삭제가 필요할 수 있습니다");
278
+ }
279
+ }
280
+ }
281
+
282
+ // ── Main Menu ──
283
+
284
+ const MENU = [
285
+ { label: "진단 (Diagnose)", hint: "읽기 전용 검사" },
286
+ { label: "수정 (Fix)", hint: "자동 수정 + 진단" },
287
+ { label: "캐시 관리 (Cache)", hint: "캐시 조회/선택 삭제" },
288
+ { label: "팀 세션 정리 (Teams)", hint: "잔존 팀 감지/정리" },
289
+ { label: "전체 초기화 (Reset)", hint: "캐시 전체 삭제 + 재생성" },
290
+ { label: "종료", hint: "Ctrl+C" },
291
+ ];
292
+
293
+ async function main() {
294
+ onExit(() => {});
295
+ clear();
296
+
297
+ while (true) {
298
+ box("triflux Doctor", 46);
299
+ console.log();
300
+
301
+ const choice = await select("작업 선택", MENU);
302
+ if (!choice || choice.index === 5) {
303
+ console.log();
304
+ info("종료합니다.");
305
+ showCursor();
306
+ break;
307
+ }
308
+
309
+ console.log();
310
+
311
+ switch (choice.index) {
312
+ case 0: {
313
+ const spin = spinner("진단 중...");
314
+ const report = runDoctor("check");
315
+ spin.stop();
316
+ showReport(report);
317
+ break;
318
+ }
319
+
320
+ case 1: {
321
+ if (!(await confirm("자동 수정을 실행하시겠습니까?"))) break;
322
+ const spin = spinner("수정 + 진단 중...");
323
+ const report = runDoctor("fix");
324
+ spin.stop();
325
+ showReport(report);
326
+ break;
327
+ }
328
+
329
+ case 2: {
330
+ showCacheStatus();
331
+ console.log();
332
+ await selectiveReset();
333
+ break;
334
+ }
335
+
336
+ case 3: {
337
+ await checkOrphanTeams();
338
+ break;
339
+ }
340
+
341
+ case 4: {
342
+ if (
343
+ !(await confirm(
344
+ `${RED}전체 캐시를 초기화${RESET}하시겠습니까?`,
345
+ false,
346
+ ))
347
+ )
348
+ break;
349
+ const spin = spinner("초기화 + 재생성 중...");
350
+ try {
351
+ execFileSync(
352
+ process.execPath,
353
+ [join(PKG_ROOT, "bin", "triflux.mjs"), "doctor", "--reset"],
354
+ { timeout: 60000, encoding: "utf8", windowsHide: true },
355
+ );
356
+ spin.stop();
357
+ ok("전체 초기화 + 재생성 완료");
358
+ } catch {
359
+ spin.stop();
360
+ warn("초기화 중 일부 실패 — triflux doctor --reset으로 재시도");
361
+ }
362
+ break;
363
+ }
364
+ }
365
+
366
+ console.log();
367
+ divider(46);
368
+ }
369
+ }
370
+
371
+ main().catch((e) => {
372
+ showCursor();
373
+ console.error(e);
374
+ process.exit(1);
375
+ });
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env node
2
+ // tui/gemini-profile.mjs — Interactive Gemini Profile Manager
3
+ // Codex config.toml 대칭 구조 — JSON 기반 프로필 CRUD
4
+ import {
5
+ copyFileSync,
6
+ existsSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+ import {
14
+ BOLD,
15
+ box,
16
+ CYAN,
17
+ clear,
18
+ confirm,
19
+ DIM,
20
+ divider,
21
+ fail,
22
+ GREEN,
23
+ info,
24
+ input,
25
+ label,
26
+ ok,
27
+ onExit,
28
+ RED,
29
+ RESET,
30
+ select,
31
+ showCursor,
32
+ table,
33
+ WHITE,
34
+ warn,
35
+ YELLOW,
36
+ } from "./core.mjs";
37
+
38
+ const GEMINI_DIR = join(homedir(), ".gemini");
39
+ const CONFIG_PATH = join(GEMINI_DIR, "triflux-profiles.json");
40
+
41
+ const KNOWN_MODELS = [
42
+ { label: "gemini-3.1-pro-preview", hint: "3.1 Pro — 플래그십" },
43
+ { label: "gemini-3-flash-preview", hint: "3.0 Flash — 빠른 응답" },
44
+ { label: "gemini-2.5-pro", hint: "2.5 Pro — 안정" },
45
+ { label: "gemini-2.5-flash", hint: "2.5 Flash — 경량" },
46
+ { label: "gemini-2.5-flash-lite", hint: "2.5 Flash Lite — 최경량" },
47
+ { label: "직접 입력", hint: "" },
48
+ ];
49
+
50
+ const DEFAULT_CONFIG = {
51
+ model: "gemini-3.1-pro-preview",
52
+ profiles: {
53
+ pro31: {
54
+ model: "gemini-3.1-pro-preview",
55
+ hint: "3.1 Pro — 플래그십 (1M ctx, 멀티모달)",
56
+ },
57
+ flash3: {
58
+ model: "gemini-3-flash-preview",
59
+ hint: "3.0 Flash — 빠른 응답, 비용 효율",
60
+ },
61
+ pro25: { model: "gemini-2.5-pro", hint: "2.5 Pro — 안정 (추론 강화)" },
62
+ flash25: { model: "gemini-2.5-flash", hint: "2.5 Flash — 경량 범용" },
63
+ lite25: { model: "gemini-2.5-flash-lite", hint: "2.5 Flash Lite — 최경량" },
64
+ },
65
+ };
66
+
67
+ // ── JSON Config ──
68
+
69
+ function readConfig() {
70
+ if (!existsSync(CONFIG_PATH)) return structuredClone(DEFAULT_CONFIG);
71
+ try {
72
+ return JSON.parse(readFileSync(CONFIG_PATH, "utf8"));
73
+ } catch {
74
+ warn("triflux-profiles.json 파싱 실패, 기본값 사용");
75
+ return structuredClone(DEFAULT_CONFIG);
76
+ }
77
+ }
78
+
79
+ function getProfiles(config) {
80
+ return Object.entries(config.profiles || {}).map(([name, p]) => ({
81
+ name,
82
+ model: p.model || config.model,
83
+ hint: p.hint || "",
84
+ }));
85
+ }
86
+
87
+ // ── UI Flows ──
88
+
89
+ function showStatus(config) {
90
+ const profiles = getProfiles(config);
91
+
92
+ console.log();
93
+ label("기본 모델", `${WHITE}${config.model || "미설정"}${RESET}`);
94
+ console.log();
95
+
96
+ if (profiles.length === 0) {
97
+ warn("등록된 프로필이 없습니다.");
98
+ return;
99
+ }
100
+
101
+ const headers = ["프로필", "모델", "설명"];
102
+ const rows = profiles.map((p) => [
103
+ `${CYAN}${p.name}${RESET}`,
104
+ modelColor(p.model),
105
+ p.hint ? `${DIM}${p.hint}${RESET}` : "",
106
+ ]);
107
+
108
+ table(headers, rows);
109
+ }
110
+
111
+ function modelColor(model) {
112
+ if (!model) return `${DIM}inherit${RESET}`;
113
+ if (model.includes("pro")) return `${YELLOW}${model}${RESET}`;
114
+ if (model.includes("flash-lite")) return `${GREEN}${model}${RESET}`;
115
+ if (model.includes("flash")) return `${CYAN}${model}${RESET}`;
116
+ return `${WHITE}${model}${RESET}`;
117
+ }
118
+
119
+ async function pickModel(current) {
120
+ const idx = KNOWN_MODELS.findIndex((m) => m.label === current);
121
+ const choice = await select("모델 선택", KNOWN_MODELS, {
122
+ initial: Math.max(0, idx),
123
+ });
124
+ if (!choice) return null;
125
+ if (choice.value.label === "직접 입력") {
126
+ return await input("모델 ID", current || "");
127
+ }
128
+ return choice.value.label;
129
+ }
130
+
131
+ async function editProfile(config) {
132
+ const profiles = getProfiles(config);
133
+ if (profiles.length === 0) {
134
+ warn("편집할 프로필이 없습니다.");
135
+ return config;
136
+ }
137
+
138
+ const options = profiles.map((p) => ({
139
+ label: p.name,
140
+ hint: `${DIM}${p.model}${RESET}`,
141
+ }));
142
+
143
+ const picked = await select("편집할 프로필", options);
144
+ if (!picked) return config;
145
+
146
+ const profile = profiles[picked.index];
147
+ console.log();
148
+ info(`현재: ${BOLD}${profile.name}${RESET} → ${profile.model}`);
149
+
150
+ const newModel = await pickModel(profile.model);
151
+ if (newModel === null) return config;
152
+
153
+ const hint = await input("설명 (Enter로 유지)", profile.hint);
154
+
155
+ console.log();
156
+ info(`변경: ${profile.model} → ${BOLD}${newModel}${RESET}`);
157
+
158
+ if (!(await confirm("저장하시겠습니까?"))) return config;
159
+
160
+ config.profiles[profile.name] = {
161
+ model: newModel,
162
+ hint: hint || profile.hint,
163
+ };
164
+ save(config);
165
+ ok(`${profile.name} 프로필 저장 완료`);
166
+ return readConfig();
167
+ }
168
+
169
+ async function editDefault(config) {
170
+ info(`현재 기본 모델: ${BOLD}${config.model || "미설정"}${RESET}`);
171
+
172
+ const newModel = await pickModel(config.model);
173
+ if (newModel === null) return config;
174
+
175
+ console.log();
176
+ info(`변경: ${config.model} → ${BOLD}${newModel}${RESET}`);
177
+
178
+ if (!(await confirm("저장하시겠습니까?"))) return config;
179
+
180
+ config.model = newModel;
181
+ save(config);
182
+ ok("기본 모델 저장 완료");
183
+ return readConfig();
184
+ }
185
+
186
+ async function addProfile(config) {
187
+ const name = await input("새 프로필 이름");
188
+ if (!name) return config;
189
+
190
+ if (config.profiles[name]) {
191
+ fail(`'${name}' 프로필이 이미 존재합니다.`);
192
+ return config;
193
+ }
194
+
195
+ const model = await pickModel("");
196
+ if (!model) return config;
197
+
198
+ const hint = await input("설명 (선택)", "");
199
+
200
+ console.log();
201
+ info(`추가: ${BOLD}${name}${RESET} → ${model}`);
202
+ if (!(await confirm("저장하시겠습니까?"))) return config;
203
+
204
+ config.profiles[name] = { model, hint };
205
+ save(config);
206
+ ok(`${name} 프로필 추가 완료`);
207
+ return readConfig();
208
+ }
209
+
210
+ async function removeProfile(config) {
211
+ const profiles = getProfiles(config);
212
+ if (profiles.length === 0) {
213
+ warn("삭제할 프로필이 없습니다.");
214
+ return config;
215
+ }
216
+
217
+ const options = profiles.map((p) => ({ label: p.name, hint: p.model }));
218
+ const picked = await select("삭제할 프로필", options);
219
+ if (!picked) return config;
220
+
221
+ const name = profiles[picked.index].name;
222
+ if (
223
+ !(await confirm(`${RED}${name}${RESET} 프로필을 삭제하시겠습니까?`, false))
224
+ ) {
225
+ return config;
226
+ }
227
+
228
+ delete config.profiles[name];
229
+ save(config);
230
+ ok(`${name} 프로필 삭제 완료`);
231
+ return readConfig();
232
+ }
233
+
234
+ function save(config) {
235
+ if (!existsSync(GEMINI_DIR)) mkdirSync(GEMINI_DIR, { recursive: true });
236
+
237
+ // Backup before write
238
+ if (existsSync(CONFIG_PATH)) {
239
+ copyFileSync(CONFIG_PATH, CONFIG_PATH + ".bak");
240
+ }
241
+
242
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf8");
243
+ }
244
+
245
+ // ── Main Loop ──
246
+
247
+ const MENU = [
248
+ { label: "프로필 모델 변경", hint: "모델 수정" },
249
+ { label: "기본 모델 변경", hint: "top-level default" },
250
+ { label: "프로필 추가", hint: "새 프로필 생성" },
251
+ { label: "프로필 삭제", hint: "기존 프로필 제거" },
252
+ { label: "종료", hint: "Ctrl+C" },
253
+ ];
254
+
255
+ async function main() {
256
+ onExit(() => {});
257
+ clear();
258
+
259
+ let config = readConfig();
260
+
261
+ while (true) {
262
+ box("Gemini Profile Manager", 46);
263
+ showStatus(config);
264
+ console.log();
265
+
266
+ const choice = await select("작업 선택", MENU);
267
+ if (!choice || choice.index === 4) {
268
+ console.log();
269
+ info("종료합니다.");
270
+ showCursor();
271
+ break;
272
+ }
273
+
274
+ console.log();
275
+ switch (choice.index) {
276
+ case 0:
277
+ config = await editProfile(config);
278
+ break;
279
+ case 1:
280
+ config = await editDefault(config);
281
+ break;
282
+ case 2:
283
+ config = await addProfile(config);
284
+ break;
285
+ case 3:
286
+ config = await removeProfile(config);
287
+ break;
288
+ }
289
+
290
+ console.log();
291
+ divider(46);
292
+ }
293
+ }
294
+
295
+ main().catch((e) => {
296
+ showCursor();
297
+ console.error(e);
298
+ process.exit(1);
299
+ });