triflux 0.0.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,707 @@
1
+ #!/usr/bin/env node
2
+ // triflux CLI — setup, doctor, version
3
+ import { copyFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync } from "fs";
4
+ import { join, dirname } from "path";
5
+ import { homedir } from "os";
6
+ import { execSync } from "child_process";
7
+
8
+ const PKG_ROOT = dirname(dirname(new URL(import.meta.url).pathname)).replace(/^\/([A-Z]:)/, "$1");
9
+ const CLAUDE_DIR = join(homedir(), ".claude");
10
+ const PKG = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
11
+
12
+ // ── 색상 체계 (triflux brand: amber/orange accent) ──
13
+ const CYAN = "\x1b[36m";
14
+ const GREEN = "\x1b[32m";
15
+ const RED = "\x1b[31m";
16
+ const YELLOW = "\x1b[33m";
17
+ const DIM = "\x1b[2m";
18
+ const BOLD = "\x1b[1m";
19
+ const RESET = "\x1b[0m";
20
+ const AMBER = "\x1b[38;5;214m";
21
+ const BLUE = "\x1b[38;5;39m";
22
+ const WHITE_BRIGHT = "\x1b[97m";
23
+ const GRAY = "\x1b[38;5;245m";
24
+ const GREEN_BRIGHT = "\x1b[38;5;82m";
25
+ const RED_BRIGHT = "\x1b[38;5;196m";
26
+
27
+ // ── 브랜드 요소 ──
28
+ const BRAND = `${AMBER}${BOLD}triflux${RESET}`;
29
+ const VER = `${DIM}v${PKG.version}${RESET}`;
30
+ const LINE = `${GRAY}${"─".repeat(48)}${RESET}`;
31
+ const DOT = `${GRAY}·${RESET}`;
32
+
33
+ // ── 유틸리티 ──
34
+
35
+ function ok(msg) { console.log(` ${GREEN_BRIGHT}✓${RESET} ${msg}`); }
36
+ function warn(msg) { console.log(` ${YELLOW}⚠${RESET} ${msg}`); }
37
+ function fail(msg) { console.log(` ${RED_BRIGHT}✗${RESET} ${msg}`); }
38
+ function info(msg) { console.log(` ${GRAY}${msg}${RESET}`); }
39
+ function section(title) { console.log(`\n ${AMBER}▸${RESET} ${BOLD}${title}${RESET}`); }
40
+
41
+ function which(cmd) {
42
+ try {
43
+ const result = execSync(
44
+ process.platform === "win32" ? `where ${cmd} 2>nul` : `which ${cmd} 2>/dev/null`,
45
+ { encoding: "utf8", timeout: 5000 }
46
+ ).trim();
47
+ return result.split(/\r?\n/)[0] || null;
48
+ } catch { return null; }
49
+ }
50
+
51
+ function whichInShell(cmd, shell) {
52
+ const cmds = {
53
+ bash: `bash -c 'command -v ${cmd} 2>/dev/null'`,
54
+ cmd: `cmd /c where ${cmd} 2>nul`,
55
+ pwsh: `pwsh -NoProfile -c "(Get-Command ${cmd} -EA SilentlyContinue).Source"`,
56
+ };
57
+ const command = cmds[shell];
58
+ if (!command) return null;
59
+ try {
60
+ const result = execSync(command, {
61
+ encoding: "utf8",
62
+ timeout: 8000,
63
+ stdio: ["pipe", "pipe", "ignore"],
64
+ }).trim();
65
+ return result.split(/\r?\n/)[0] || null;
66
+ } catch { return null; }
67
+ }
68
+
69
+ function checkShellAvailable(shell) {
70
+ const cmds = { bash: "bash --version", cmd: "cmd /c echo ok", pwsh: "pwsh -NoProfile -c echo ok" };
71
+ try {
72
+ execSync(cmds[shell], { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "ignore"] });
73
+ return true;
74
+ } catch { return false; }
75
+ }
76
+
77
+ function getVersion(filePath) {
78
+ try {
79
+ const content = readFileSync(filePath, "utf8");
80
+ const match = content.match(/VERSION\s*=\s*"([^"]+)"/);
81
+ return match ? match[1] : null;
82
+ } catch { return null; }
83
+ }
84
+
85
+ function syncFile(src, dst, label) {
86
+ const dstDir = dirname(dst);
87
+ if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
88
+
89
+ if (!existsSync(src)) {
90
+ fail(`${label}: 소스 파일 없음 (${src})`);
91
+ return false;
92
+ }
93
+
94
+ const srcVer = getVersion(src);
95
+ const dstVer = existsSync(dst) ? getVersion(dst) : null;
96
+
97
+ if (!existsSync(dst)) {
98
+ copyFileSync(src, dst);
99
+ try { chmodSync(dst, 0o755); } catch {}
100
+ ok(`${label}: 설치됨 ${srcVer ? `(v${srcVer})` : ""}`);
101
+ return true;
102
+ }
103
+
104
+ const srcContent = readFileSync(src, "utf8");
105
+ const dstContent = readFileSync(dst, "utf8");
106
+ if (srcContent !== dstContent) {
107
+ copyFileSync(src, dst);
108
+ try { chmodSync(dst, 0o755); } catch {}
109
+ const verInfo = (srcVer && dstVer && srcVer !== dstVer)
110
+ ? `(v${dstVer} → v${srcVer})`
111
+ : srcVer ? `(v${srcVer}, 내용 변경)` : "(내용 변경)";
112
+ ok(`${label}: 업데이트됨 ${verInfo}`);
113
+ return true;
114
+ }
115
+
116
+ ok(`${label}: 최신 상태 ${srcVer ? `(v${srcVer})` : ""}`);
117
+ return false;
118
+ }
119
+
120
+ // ── 크로스 셸 진단 ──
121
+
122
+ function checkCliCrossShell(cmd, installHint) {
123
+ const shells = process.platform === "win32" ? ["bash", "cmd", "pwsh"] : ["bash"];
124
+ let anyFound = false;
125
+ let bashMissing = false;
126
+
127
+ for (const shell of shells) {
128
+ if (!checkShellAvailable(shell)) {
129
+ info(`${shell}: ${DIM}셸 없음 (건너뜀)${RESET}`);
130
+ continue;
131
+ }
132
+ const p = whichInShell(cmd, shell);
133
+ if (p) {
134
+ ok(`${shell}: ${p}`);
135
+ anyFound = true;
136
+ } else {
137
+ fail(`${shell}: 미발견`);
138
+ if (shell === "bash") bashMissing = true;
139
+ }
140
+ }
141
+
142
+ if (!anyFound) {
143
+ info(`미설치 (선택사항) — ${installHint}`);
144
+ info("없으면 Claude 네이티브 에이전트로 fallback");
145
+ return 1;
146
+ }
147
+ if (bashMissing) {
148
+ warn("bash에서 미발견 — cli-route.sh 실행 불가");
149
+ info('→ ~/.bashrc에 추가: export PATH="$PATH:$APPDATA/npm"');
150
+ return 1;
151
+ }
152
+ return 0;
153
+ }
154
+
155
+ // ── 명령어 ──
156
+
157
+ function cmdSetup() {
158
+ console.log(`\n${BOLD}triflux setup${RESET}\n`);
159
+
160
+ syncFile(
161
+ join(PKG_ROOT, "scripts", "cli-route.sh"),
162
+ join(CLAUDE_DIR, "scripts", "cli-route.sh"),
163
+ "cli-route.sh"
164
+ );
165
+
166
+ syncFile(
167
+ join(PKG_ROOT, "hud", "hud-qos-status.mjs"),
168
+ join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"),
169
+ "hud-qos-status.mjs"
170
+ );
171
+
172
+ // 스킬 동기화 (~/.claude/skills/{name}/SKILL.md)
173
+ const skillsSrc = join(PKG_ROOT, "skills");
174
+ const skillsDst = join(CLAUDE_DIR, "skills");
175
+ if (existsSync(skillsSrc)) {
176
+ let skillCount = 0;
177
+ let skillTotal = 0;
178
+ for (const name of readdirSync(skillsSrc)) {
179
+ const src = join(skillsSrc, name, "SKILL.md");
180
+ const dst = join(skillsDst, name, "SKILL.md");
181
+ if (!existsSync(src)) continue;
182
+ skillTotal++;
183
+
184
+ const dstDir = dirname(dst);
185
+ if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
186
+
187
+ if (!existsSync(dst)) {
188
+ copyFileSync(src, dst);
189
+ skillCount++;
190
+ } else {
191
+ const srcContent = readFileSync(src, "utf8");
192
+ const dstContent = readFileSync(dst, "utf8");
193
+ if (srcContent !== dstContent) {
194
+ copyFileSync(src, dst);
195
+ skillCount++;
196
+ }
197
+ }
198
+ }
199
+ if (skillCount > 0) {
200
+ ok(`스킬: ${skillCount}/${skillTotal}개 업데이트됨`);
201
+ } else {
202
+ ok(`스킬: ${skillTotal}개 최신 상태`);
203
+ }
204
+ }
205
+
206
+ // HUD statusLine 설정
207
+ console.log(`${CYAN}[HUD 설정]${RESET}`);
208
+ const settingsPath = join(CLAUDE_DIR, "settings.json");
209
+ const hudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
210
+
211
+ if (existsSync(hudPath)) {
212
+ try {
213
+ let settings = {};
214
+ if (existsSync(settingsPath)) {
215
+ settings = JSON.parse(readFileSync(settingsPath, "utf8"));
216
+ }
217
+
218
+ const currentCmd = settings.statusLine?.command || "";
219
+ if (currentCmd.includes("hud-qos-status.mjs")) {
220
+ ok("statusLine 이미 설정됨");
221
+ } else {
222
+ const nodePath = process.execPath.replace(/\\/g, "/");
223
+ const hudForward = hudPath.replace(/\\/g, "/");
224
+ const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
225
+ const hudRef = hudForward.includes(" ") ? `"${hudForward}"` : hudForward;
226
+
227
+ if (currentCmd) {
228
+ warn(`기존 statusLine 덮어쓰기: ${currentCmd}`);
229
+ }
230
+
231
+ settings.statusLine = {
232
+ type: "command",
233
+ command: `${nodeRef} ${hudRef}`,
234
+ };
235
+
236
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
237
+ ok("statusLine 설정 완료 — 세션 재시작 후 HUD 표시");
238
+ }
239
+ } catch (e) {
240
+ fail(`settings.json 처리 실패: ${e.message}`);
241
+ }
242
+ } else {
243
+ warn("HUD 파일 없음 — 먼저 파일 동기화 필요");
244
+ }
245
+
246
+ console.log(`\n${DIM}설치 위치: ${CLAUDE_DIR}${RESET}\n`);
247
+ }
248
+
249
+ function cmdDoctor() {
250
+ console.log(`\n ${AMBER}${BOLD}⬡ triflux doctor${RESET} ${VER}\n`);
251
+ console.log(` ${LINE}`);
252
+ let issues = 0;
253
+
254
+ // 1. cli-route.sh
255
+ section("cli-route.sh");
256
+ const routeSh = join(CLAUDE_DIR, "scripts", "cli-route.sh");
257
+ if (existsSync(routeSh)) {
258
+ const ver = getVersion(routeSh);
259
+ ok(`설치됨 ${ver ? `${DIM}v${ver}${RESET}` : ""}`);
260
+ } else {
261
+ fail("미설치 — tfx setup 실행 필요");
262
+ issues++;
263
+ }
264
+
265
+ // 2. HUD
266
+ section("HUD");
267
+ const hud = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
268
+ if (existsSync(hud)) {
269
+ ok("설치됨");
270
+ } else {
271
+ warn("미설치 ${GRAY}(선택사항)${RESET}");
272
+ }
273
+
274
+ // 3. Codex CLI
275
+ section(`Codex CLI ${WHITE_BRIGHT}●${RESET}`);
276
+ issues += checkCliCrossShell("codex", "npm install -g @openai/codex");
277
+ if (which("codex")) {
278
+ if (process.env.OPENAI_API_KEY) {
279
+ ok("OPENAI_API_KEY 설정됨");
280
+ } else {
281
+ warn(`OPENAI_API_KEY 미설정 ${GRAY}(Pro 구독이면 불필요)${RESET}`);
282
+ }
283
+ }
284
+
285
+ // 4. Gemini CLI
286
+ section(`Gemini CLI ${BLUE}●${RESET}`);
287
+ issues += checkCliCrossShell("gemini", "npm install -g @google/gemini-cli");
288
+ if (which("gemini")) {
289
+ if (process.env.GEMINI_API_KEY) {
290
+ ok("GEMINI_API_KEY 설정됨");
291
+ } else {
292
+ warn(`GEMINI_API_KEY 미설정 ${GRAY}(gemini auth login)${RESET}`);
293
+ }
294
+ }
295
+
296
+ // 5. Claude Code
297
+ section(`Claude Code ${AMBER}●${RESET}`);
298
+ const claudePath = which("claude");
299
+ if (claudePath) {
300
+ ok("설치됨");
301
+ } else {
302
+ fail("미설치 (필수)");
303
+ issues++;
304
+ }
305
+
306
+ // 6. 스킬 설치 상태
307
+ section("Skills");
308
+ const skillsSrc = join(PKG_ROOT, "skills");
309
+ const skillsDst = join(CLAUDE_DIR, "skills");
310
+ if (existsSync(skillsSrc)) {
311
+ let installed = 0;
312
+ let total = 0;
313
+ const missing = [];
314
+ for (const name of readdirSync(skillsSrc)) {
315
+ if (!existsSync(join(skillsSrc, name, "SKILL.md"))) continue;
316
+ total++;
317
+ if (existsSync(join(skillsDst, name, "SKILL.md"))) {
318
+ installed++;
319
+ } else {
320
+ missing.push(name);
321
+ }
322
+ }
323
+ if (installed === total) {
324
+ ok(`${installed}/${total}개 설치됨`);
325
+ } else {
326
+ warn(`${installed}/${total}개 설치됨 — 미설치: ${missing.join(", ")}`);
327
+ info("triflux setup으로 동기화 가능");
328
+ issues++;
329
+ }
330
+ }
331
+
332
+ // 7. 플러그인 등록
333
+ section("Plugin");
334
+ const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
335
+ if (existsSync(pluginsFile)) {
336
+ const content = readFileSync(pluginsFile, "utf8");
337
+ if (content.includes("triflux")) {
338
+ ok("triflux 플러그인 등록됨");
339
+ } else {
340
+ warn("triflux 플러그인 미등록 — npm 단독 사용 중");
341
+ info("플러그인 등록: /plugin marketplace add <repo-url>");
342
+ }
343
+ } else {
344
+ info("플러그인 시스템 감지 안 됨 — npm 단독 사용");
345
+ }
346
+
347
+ // 8. MCP 인벤토리
348
+ section("MCP Inventory");
349
+ const mcpCache = join(CLAUDE_DIR, "cache", "mcp-inventory.json");
350
+ if (existsSync(mcpCache)) {
351
+ try {
352
+ const inv = JSON.parse(readFileSync(mcpCache, "utf8"));
353
+ ok(`캐시 존재 (${inv.timestamp})`);
354
+ if (inv.codex?.servers?.length) {
355
+ const names = inv.codex.servers.map(s => s.name).join(", ");
356
+ info(`Codex: ${inv.codex.servers.length}개 서버 (${names})`);
357
+ }
358
+ if (inv.gemini?.servers?.length) {
359
+ const names = inv.gemini.servers.map(s => s.name).join(", ");
360
+ info(`Gemini: ${inv.gemini.servers.length}개 서버 (${names})`);
361
+ }
362
+ } catch {
363
+ warn("캐시 파일 파싱 실패");
364
+ }
365
+ } else {
366
+ warn("캐시 없음 — 다음 세션 시작 시 자동 생성");
367
+ info(`수동: node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}`);
368
+ }
369
+
370
+ // 9. CLI 이슈 트래커
371
+ section("CLI Issues");
372
+ const issuesFile = join(CLAUDE_DIR, "cache", "cli-issues.jsonl");
373
+ if (existsSync(issuesFile)) {
374
+ try {
375
+ const lines = readFileSync(issuesFile, "utf8").trim().split("\n").filter(Boolean);
376
+ const entries = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
377
+ const unresolved = entries.filter(e => !e.resolved);
378
+
379
+ if (unresolved.length === 0) {
380
+ ok("미해결 이슈 없음");
381
+ } else {
382
+ // 패턴별 그룹핑
383
+ const groups = {};
384
+ for (const e of unresolved) {
385
+ const key = `${e.cli}:${e.pattern}`;
386
+ if (!groups[key]) groups[key] = { ...e, count: 0 };
387
+ groups[key].count++;
388
+ if (e.ts > groups[key].ts) { groups[key].ts = e.ts; groups[key].snippet = e.snippet; }
389
+ }
390
+
391
+ // 알려진 해결 버전 (패턴별 수정된 triflux 버전)
392
+ const KNOWN_FIXES = {
393
+ "gemini:deprecated_flag": "1.8.9", // -p → --prompt
394
+ };
395
+
396
+ const currentVer = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8")).version;
397
+ let cleaned = 0;
398
+
399
+ for (const [key, g] of Object.entries(groups)) {
400
+ const fixVer = KNOWN_FIXES[key];
401
+ if (fixVer && currentVer >= fixVer) {
402
+ // 해결된 이슈 — 자동 정리
403
+ cleaned += g.count;
404
+ continue;
405
+ }
406
+ const age = Date.now() - g.ts;
407
+ const ago = age < 3600000 ? `${Math.round(age / 60000)}분 전` :
408
+ age < 86400000 ? `${Math.round(age / 3600000)}시간 전` :
409
+ `${Math.round(age / 86400000)}일 전`;
410
+ const sev = g.severity === "error" ? `${RED}ERROR${RESET}` : `${YELLOW}WARN${RESET}`;
411
+ warn(`[${sev}] ${g.cli}/${g.pattern} x${g.count} (최근: ${ago})`);
412
+ if (g.snippet) info(` ${g.snippet.substring(0, 120)}`);
413
+ if (fixVer) info(` 해결: triflux >= v${fixVer} (npm update -g triflux)`);
414
+ issues++;
415
+ }
416
+
417
+ // 해결된 이슈 자동 정리
418
+ if (cleaned > 0) {
419
+ const remaining = entries.filter(e => {
420
+ const key = `${e.cli}:${e.pattern}`;
421
+ const fixVer = KNOWN_FIXES[key];
422
+ return !(fixVer && currentVer >= fixVer);
423
+ });
424
+ writeFileSync(issuesFile, remaining.map(e => JSON.stringify(e)).join("\n") + (remaining.length ? "\n" : ""));
425
+ ok(`${cleaned}개 해결된 이슈 자동 정리됨`);
426
+ }
427
+ }
428
+ } catch (e) {
429
+ warn(`이슈 파일 읽기 실패: ${e.message}`);
430
+ }
431
+ } else {
432
+ ok("이슈 로그 없음 (정상)");
433
+ }
434
+
435
+ // 결과
436
+ console.log(`\n ${LINE}`);
437
+ if (issues === 0) {
438
+ console.log(` ${GREEN_BRIGHT}${BOLD}✓ 모든 검사 통과${RESET}\n`);
439
+ } else {
440
+ console.log(` ${YELLOW}${BOLD}⚠ ${issues}개 항목 확인 필요${RESET}\n`);
441
+ }
442
+ }
443
+
444
+ function cmdUpdate() {
445
+ console.log(`\n${BOLD}triflux update${RESET}\n`);
446
+
447
+ // 1. 설치 방식 감지
448
+ const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
449
+ let installMode = "unknown";
450
+ let pluginPath = null;
451
+
452
+ // 플러그인 모드 감지
453
+ if (existsSync(pluginsFile)) {
454
+ try {
455
+ const plugins = JSON.parse(readFileSync(pluginsFile, "utf8"));
456
+ for (const [key, entries] of Object.entries(plugins.plugins || {})) {
457
+ if (key.startsWith("triflux")) {
458
+ pluginPath = entries[0]?.installPath;
459
+ installMode = "plugin";
460
+ break;
461
+ }
462
+ }
463
+ } catch {}
464
+ }
465
+
466
+ // PKG_ROOT가 플러그인 캐시 내에 있으면 플러그인 모드
467
+ if (installMode === "unknown" && PKG_ROOT.includes(join(".claude", "plugins"))) {
468
+ installMode = "plugin";
469
+ pluginPath = PKG_ROOT;
470
+ }
471
+
472
+ // npm global 감지
473
+ if (installMode === "unknown") {
474
+ try {
475
+ const npmList = execSync("npm list -g triflux --depth=0", {
476
+ encoding: "utf8",
477
+ timeout: 10000,
478
+ stdio: ["pipe", "pipe", "ignore"],
479
+ });
480
+ if (npmList.includes("triflux")) installMode = "npm-global";
481
+ } catch {}
482
+ }
483
+
484
+ // npm local 감지
485
+ if (installMode === "unknown") {
486
+ const localPkg = join(process.cwd(), "node_modules", "triflux");
487
+ if (existsSync(localPkg)) installMode = "npm-local";
488
+ }
489
+
490
+ // git 저장소 직접 사용
491
+ if (installMode === "unknown" && existsSync(join(PKG_ROOT, ".git"))) {
492
+ installMode = "git-local";
493
+ }
494
+
495
+ info(`검색: ${installMode === "plugin" ? "플러그인" : installMode === "npm-global" ? "npm global" : installMode === "npm-local" ? "npm local" : installMode === "git-local" ? "git 로컬 저장소" : "알 수 없음"} 설치 감지`);
496
+
497
+ // 2. 설치 방식에 따라 업데이트
498
+ const oldVer = PKG.version;
499
+ let updated = false;
500
+
501
+ try {
502
+ switch (installMode) {
503
+ case "plugin": {
504
+ const gitDir = pluginPath || PKG_ROOT;
505
+ const result = execSync("git pull", {
506
+ encoding: "utf8",
507
+ timeout: 30000,
508
+ cwd: gitDir,
509
+ }).trim();
510
+ ok(`git pull — ${result}`);
511
+ updated = true;
512
+ break;
513
+ }
514
+ case "npm-global": {
515
+ const result = execSync("npm update -g triflux", {
516
+ encoding: "utf8",
517
+ timeout: 60000,
518
+ stdio: ["pipe", "pipe", "ignore"],
519
+ }).trim().split(/\r?\n/)[0];
520
+ ok(`npm update -g — ${result || "완료"}`);
521
+ updated = true;
522
+ break;
523
+ }
524
+ case "npm-local": {
525
+ const result = execSync("npm update triflux", {
526
+ encoding: "utf8",
527
+ timeout: 60000,
528
+ cwd: process.cwd(),
529
+ stdio: ["pipe", "pipe", "ignore"],
530
+ }).trim().split(/\r?\n/)[0];
531
+ ok(`npm update — ${result || "완료"}`);
532
+ updated = true;
533
+ break;
534
+ }
535
+ case "git-local": {
536
+ const result = execSync("git pull", {
537
+ encoding: "utf8",
538
+ timeout: 30000,
539
+ cwd: PKG_ROOT,
540
+ }).trim();
541
+ ok(`git pull — ${result}`);
542
+ updated = true;
543
+ break;
544
+ }
545
+ default:
546
+ fail("설치 방식을 감지할 수 없음");
547
+ info("수동 업데이트: cd <triflux-dir> && git pull");
548
+ return;
549
+ }
550
+ } catch (e) {
551
+ fail(`업데이트 실패: ${e.message}`);
552
+ return;
553
+ }
554
+
555
+ // 3. setup 재실행 (cli-route.sh, HUD, 스킬 동기화)
556
+ if (updated) {
557
+ console.log("");
558
+ // 업데이트 후 새 버전 읽기
559
+ let newVer = oldVer;
560
+ try {
561
+ const newPkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
562
+ newVer = newPkg.version;
563
+ } catch {}
564
+
565
+ if (newVer !== oldVer) {
566
+ ok(`버전: v${oldVer} → v${newVer}`);
567
+ } else {
568
+ ok(`버전: v${oldVer} (이미 최신)`);
569
+ }
570
+
571
+ // setup 재실행
572
+ console.log("");
573
+ info("setup 재실행 중...");
574
+ cmdSetup();
575
+ }
576
+
577
+ console.log(`${GREEN}${BOLD}업데이트 완료${RESET}\n`);
578
+ }
579
+
580
+ function cmdList() {
581
+ console.log(`\n ${AMBER}${BOLD}⬡ triflux list${RESET} ${VER}\n`);
582
+ console.log(` ${LINE}`);
583
+
584
+ const pluginSkills = join(PKG_ROOT, "skills");
585
+ const installedSkills = join(CLAUDE_DIR, "skills");
586
+
587
+ section("패키지 스킬");
588
+ if (existsSync(pluginSkills)) {
589
+ for (const name of readdirSync(pluginSkills).sort()) {
590
+ const src = join(pluginSkills, name, "SKILL.md");
591
+ if (!existsSync(src)) continue;
592
+ const dst = join(installedSkills, name, "SKILL.md");
593
+ const installed = existsSync(dst);
594
+ if (installed) {
595
+ console.log(` ${GREEN_BRIGHT}✓${RESET} ${BOLD}${name}${RESET}`);
596
+ } else {
597
+ console.log(` ${RED_BRIGHT}✗${RESET} ${DIM}${name}${RESET} ${GRAY}(미설치)${RESET}`);
598
+ }
599
+ }
600
+ }
601
+
602
+ section("사용자 스킬");
603
+ const pkgNames = new Set(existsSync(pluginSkills) ? readdirSync(pluginSkills) : []);
604
+ let userCount = 0;
605
+ if (existsSync(installedSkills)) {
606
+ for (const name of readdirSync(installedSkills).sort()) {
607
+ if (pkgNames.has(name)) continue;
608
+ const skill = join(installedSkills, name, "SKILL.md");
609
+ if (!existsSync(skill)) continue;
610
+ console.log(` ${AMBER}◆${RESET} ${name}`);
611
+ userCount++;
612
+ }
613
+ }
614
+ if (userCount === 0) console.log(` ${GRAY}없음${RESET}`);
615
+
616
+ console.log(`\n ${LINE}`);
617
+ console.log(` ${GRAY}${installedSkills}${RESET}\n`);
618
+ }
619
+
620
+ function cmdVersion() {
621
+ const routeVer = getVersion(join(CLAUDE_DIR, "scripts", "cli-route.sh"));
622
+ const hudVer = getVersion(join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"));
623
+ console.log(`\n ${AMBER}${BOLD}⬡ triflux${RESET} ${WHITE_BRIGHT}v${PKG.version}${RESET}`);
624
+ if (routeVer) console.log(` ${GRAY}cli-route${RESET} v${routeVer}`);
625
+ if (hudVer) console.log(` ${GRAY}hud${RESET} v${hudVer}`);
626
+ console.log("");
627
+ }
628
+
629
+ function checkForUpdate() {
630
+ const cacheFile = join(CLAUDE_DIR, "cache", "triflux-update-check.json");
631
+ const cacheDir = dirname(cacheFile);
632
+
633
+ // 캐시 확인 (1시간 이내면 캐시 사용)
634
+ try {
635
+ if (existsSync(cacheFile)) {
636
+ const cache = JSON.parse(readFileSync(cacheFile, "utf8"));
637
+ if (Date.now() - cache.timestamp < 3600000) {
638
+ return cache.latest !== PKG.version ? cache.latest : null;
639
+ }
640
+ }
641
+ } catch {}
642
+
643
+ // npm registry 조회
644
+ try {
645
+ const result = execSync("npm view triflux version", {
646
+ encoding: "utf8",
647
+ timeout: 5000,
648
+ stdio: ["pipe", "pipe", "ignore"],
649
+ }).trim();
650
+
651
+ if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true });
652
+ writeFileSync(cacheFile, JSON.stringify({ latest: result, timestamp: Date.now() }));
653
+
654
+ return result !== PKG.version ? result : null;
655
+ } catch {
656
+ return null;
657
+ }
658
+ }
659
+
660
+ function cmdHelp() {
661
+ const latestVer = checkForUpdate();
662
+ const updateNotice = latestVer
663
+ ? `\n ${YELLOW}${BOLD}↑ v${latestVer} 사용 가능${RESET} ${GRAY}npm update -g triflux${RESET}\n`
664
+ : "";
665
+
666
+ console.log(`
667
+ ${AMBER}${BOLD}⬡ triflux${RESET} ${DIM}v${PKG.version}${RESET}
668
+ ${GRAY}CLI-first multi-model orchestrator for Claude Code${RESET}
669
+ ${updateNotice}
670
+ ${LINE}
671
+
672
+ ${BOLD}Commands${RESET}
673
+
674
+ ${WHITE_BRIGHT}tfx setup${RESET} ${GRAY}파일 동기화 + HUD 설정${RESET}
675
+ ${WHITE_BRIGHT}tfx doctor${RESET} ${GRAY}CLI 진단 + 이슈 확인${RESET}
676
+ ${WHITE_BRIGHT}tfx update${RESET} ${GRAY}최신 버전으로 업데이트${RESET}
677
+ ${WHITE_BRIGHT}tfx list${RESET} ${GRAY}설치된 스킬 목록${RESET}
678
+ ${WHITE_BRIGHT}tfx version${RESET} ${GRAY}버전 표시${RESET}
679
+
680
+ ${BOLD}Skills${RESET} ${GRAY}(Claude Code 슬래시 커맨드)${RESET}
681
+
682
+ ${AMBER}/tfx-auto${RESET} ${GRAY}자동 분류 + 병렬 실행${RESET}
683
+ ${WHITE_BRIGHT}/tfx-codex${RESET} ${GRAY}Codex 전용 모드${RESET}
684
+ ${BLUE}/tfx-gemini${RESET} ${GRAY}Gemini 전용 모드${RESET}
685
+ ${AMBER}/tfx-setup${RESET} ${GRAY}HUD 설정 + 진단${RESET}
686
+
687
+ ${LINE}
688
+ ${GRAY}github.com/tellang/triflux${RESET}
689
+ `);
690
+ }
691
+
692
+ // ── 메인 ──
693
+
694
+ const cmd = process.argv[2] || "help";
695
+
696
+ switch (cmd) {
697
+ case "setup": cmdSetup(); break;
698
+ case "doctor": cmdDoctor(); break;
699
+ case "update": cmdUpdate(); break;
700
+ case "list": case "ls": cmdList(); break;
701
+ case "version": case "--version": case "-v": cmdVersion(); break;
702
+ case "help": case "--help": case "-h": cmdHelp(); break;
703
+ default:
704
+ console.error(`알 수 없는 명령: ${cmd}`);
705
+ cmdHelp();
706
+ process.exit(1);
707
+ }