triflux 3.2.0-dev.9 → 3.3.0-dev.3
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 +1516 -1386
- package/hooks/hooks.json +22 -0
- package/hooks/keyword-rules.json +4 -4
- package/hooks/pipeline-stop.mjs +54 -0
- package/hub/bridge.mjs +120 -13
- package/hub/pipe.mjs +23 -0
- package/hub/pipeline/index.mjs +121 -0
- package/hub/pipeline/state.mjs +164 -0
- package/hub/pipeline/transitions.mjs +114 -0
- package/hub/router.mjs +322 -1
- package/hub/schema.sql +49 -2
- package/hub/server.mjs +173 -8
- package/hub/store.mjs +259 -1
- package/hub/team/cli-team-control.mjs +381 -381
- package/hub/team/cli-team-start.mjs +474 -470
- package/hub/team/cli-team-status.mjs +238 -238
- package/hub/team/cli.mjs +86 -86
- package/hub/team/native.mjs +144 -6
- package/hub/team/nativeProxy.mjs +51 -38
- package/hub/team/orchestrator.mjs +15 -20
- package/hub/team/pane.mjs +101 -90
- package/hub/team/psmux.mjs +721 -72
- package/hub/team/session.mjs +450 -450
- package/hub/tools.mjs +223 -63
- package/hub/workers/delegator-mcp.mjs +900 -0
- package/hub/workers/factory.mjs +3 -0
- package/hub/workers/interface.mjs +2 -2
- package/hud/hud-qos-status.mjs +89 -144
- package/package.json +1 -1
- package/scripts/__tests__/keyword-detector.test.mjs +11 -11
- package/scripts/__tests__/smoke.test.mjs +34 -0
- package/scripts/hub-ensure.mjs +21 -3
- package/scripts/preflight-cache.mjs +72 -0
- package/scripts/setup.mjs +23 -11
- package/scripts/tfx-route.sh +74 -15
- package/skills/{tfx-team → tfx-multi}/SKILL.md +115 -32
package/bin/triflux.mjs
CHANGED
|
@@ -1,112 +1,111 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// triflux CLI — setup, doctor, version
|
|
2
|
+
// triflux CLI — setup, doctor, version
|
|
3
3
|
import { copyFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync, rmSync, statSync } from "fs";
|
|
4
4
|
import { join, dirname } from "path";
|
|
5
5
|
import { homedir } from "os";
|
|
6
|
-
import { execSync, spawn } from "child_process";
|
|
6
|
+
import { execSync, execFileSync, spawn } from "child_process";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import { setTimeout as delay } from "node:timers/promises";
|
|
9
9
|
import { detectMultiplexer, getSessionAttachedCount, killSession, listSessions, tmuxExec } from "../hub/team/session.mjs";
|
|
10
10
|
import { cleanupStaleOmcTeams, inspectStaleOmcTeams } from "../hub/team/staleState.mjs";
|
|
11
|
-
|
|
12
|
-
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
13
|
-
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
14
|
-
const CODEX_DIR = join(homedir(), ".codex");
|
|
15
|
-
const CODEX_CONFIG_PATH = join(CODEX_DIR, "config.toml");
|
|
16
|
-
const PKG = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
17
|
-
|
|
18
|
-
const REQUIRED_CODEX_PROFILES = [
|
|
19
|
-
{
|
|
20
|
-
name: "xhigh",
|
|
21
|
-
lines: [
|
|
22
|
-
'model = "gpt-5.3-codex"',
|
|
23
|
-
'model_reasoning_effort = "xhigh"',
|
|
24
|
-
],
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
name: "spark_fast",
|
|
28
|
-
lines: [
|
|
29
|
-
'model = "gpt-5.1-codex-mini"',
|
|
30
|
-
'model_reasoning_effort = "low"',
|
|
31
|
-
],
|
|
32
|
-
},
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
// ── 색상 체계 (triflux brand: amber/orange accent) ──
|
|
36
|
-
const CYAN = "\x1b[36m";
|
|
37
|
-
const GREEN = "\x1b[32m";
|
|
38
|
-
const RED = "\x1b[31m";
|
|
39
|
-
const YELLOW = "\x1b[33m";
|
|
40
|
-
const DIM = "\x1b[2m";
|
|
41
|
-
const BOLD = "\x1b[1m";
|
|
42
|
-
const RESET = "\x1b[0m";
|
|
43
|
-
const AMBER = "\x1b[38;5;214m";
|
|
44
|
-
const BLUE = "\x1b[38;5;39m";
|
|
45
|
-
const WHITE_BRIGHT = "\x1b[97m";
|
|
46
|
-
const GRAY = "\x1b[38;5;245m";
|
|
47
|
-
const GREEN_BRIGHT = "\x1b[38;5;82m";
|
|
48
|
-
const RED_BRIGHT = "\x1b[38;5;196m";
|
|
49
|
-
|
|
50
|
-
// ── 브랜드 요소 ──
|
|
51
|
-
const BRAND = `${AMBER}${BOLD}triflux${RESET}`;
|
|
11
|
+
|
|
12
|
+
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
13
|
+
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
14
|
+
const CODEX_DIR = join(homedir(), ".codex");
|
|
15
|
+
const CODEX_CONFIG_PATH = join(CODEX_DIR, "config.toml");
|
|
16
|
+
const PKG = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
17
|
+
|
|
18
|
+
const REQUIRED_CODEX_PROFILES = [
|
|
19
|
+
{
|
|
20
|
+
name: "xhigh",
|
|
21
|
+
lines: [
|
|
22
|
+
'model = "gpt-5.3-codex"',
|
|
23
|
+
'model_reasoning_effort = "xhigh"',
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "spark_fast",
|
|
28
|
+
lines: [
|
|
29
|
+
'model = "gpt-5.1-codex-mini"',
|
|
30
|
+
'model_reasoning_effort = "low"',
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// ── 색상 체계 (triflux brand: amber/orange accent) ──
|
|
36
|
+
const CYAN = "\x1b[36m";
|
|
37
|
+
const GREEN = "\x1b[32m";
|
|
38
|
+
const RED = "\x1b[31m";
|
|
39
|
+
const YELLOW = "\x1b[33m";
|
|
40
|
+
const DIM = "\x1b[2m";
|
|
41
|
+
const BOLD = "\x1b[1m";
|
|
42
|
+
const RESET = "\x1b[0m";
|
|
43
|
+
const AMBER = "\x1b[38;5;214m";
|
|
44
|
+
const BLUE = "\x1b[38;5;39m";
|
|
45
|
+
const WHITE_BRIGHT = "\x1b[97m";
|
|
46
|
+
const GRAY = "\x1b[38;5;245m";
|
|
47
|
+
const GREEN_BRIGHT = "\x1b[38;5;82m";
|
|
48
|
+
const RED_BRIGHT = "\x1b[38;5;196m";
|
|
49
|
+
|
|
50
|
+
// ── 브랜드 요소 ──
|
|
51
|
+
const BRAND = `${AMBER}${BOLD}triflux${RESET}`;
|
|
52
52
|
const VER = `${DIM}v${PKG.version}${RESET}`;
|
|
53
53
|
const LINE = `${GRAY}${"─".repeat(48)}${RESET}`;
|
|
54
54
|
const DOT = `${GRAY}·${RESET}`;
|
|
55
55
|
const STALE_TEAM_MAX_AGE_SEC = 3600;
|
|
56
|
-
|
|
57
|
-
// ── 유틸리티 ──
|
|
58
|
-
|
|
59
|
-
function ok(msg) { console.log(` ${GREEN_BRIGHT}✓${RESET} ${msg}`); }
|
|
60
|
-
function warn(msg) { console.log(` ${YELLOW}⚠${RESET} ${msg}`); }
|
|
61
|
-
function fail(msg) { console.log(` ${RED_BRIGHT}✗${RESET} ${msg}`); }
|
|
62
|
-
function info(msg) { console.log(` ${GRAY}${msg}${RESET}`); }
|
|
63
|
-
function section(title) { console.log(`\n ${AMBER}▸${RESET} ${BOLD}${title}${RESET}`); }
|
|
64
|
-
|
|
65
|
-
function which(cmd) {
|
|
66
|
-
try {
|
|
67
|
-
const result =
|
|
68
|
-
|
|
69
|
-
{ encoding: "utf8", timeout: 5000 }
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
56
|
+
|
|
57
|
+
// ── 유틸리티 ──
|
|
58
|
+
|
|
59
|
+
function ok(msg) { console.log(` ${GREEN_BRIGHT}✓${RESET} ${msg}`); }
|
|
60
|
+
function warn(msg) { console.log(` ${YELLOW}⚠${RESET} ${msg}`); }
|
|
61
|
+
function fail(msg) { console.log(` ${RED_BRIGHT}✗${RESET} ${msg}`); }
|
|
62
|
+
function info(msg) { console.log(` ${GRAY}${msg}${RESET}`); }
|
|
63
|
+
function section(title) { console.log(`\n ${AMBER}▸${RESET} ${BOLD}${title}${RESET}`); }
|
|
64
|
+
|
|
65
|
+
function which(cmd) {
|
|
66
|
+
try {
|
|
67
|
+
const result = process.platform === "win32"
|
|
68
|
+
? execFileSync("where", [cmd], { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "ignore"] })
|
|
69
|
+
: execFileSync("which", [cmd], { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "ignore"] });
|
|
70
|
+
return result.trim().split(/\r?\n/)[0] || null;
|
|
71
|
+
} catch { return null; }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function whichInShell(cmd, shell) {
|
|
75
|
+
const shellArgs = {
|
|
76
|
+
bash: ["bash", ["-c", `source ~/.bashrc 2>/dev/null && command -v "${cmd}" 2>/dev/null`]],
|
|
77
|
+
cmd: ["cmd", ["/c", "where", cmd]],
|
|
78
|
+
pwsh: ["pwsh", ["-NoProfile", "-c", `(Get-Command '${cmd.replace(/'/g, "''")}' -EA SilentlyContinue).Source`]],
|
|
79
|
+
};
|
|
80
|
+
const entry = shellArgs[shell];
|
|
81
|
+
if (!entry) return null;
|
|
82
|
+
try {
|
|
83
|
+
const result = execFileSync(entry[0], entry[1], {
|
|
84
|
+
encoding: "utf8",
|
|
85
|
+
timeout: 8000,
|
|
86
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
87
|
+
}).trim();
|
|
88
|
+
return result.split(/\r?\n/)[0] || null;
|
|
89
|
+
} catch { return null; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isDevUpdateRequested(argv = process.argv) {
|
|
93
|
+
return argv.includes("--dev") || argv.includes("@dev") || argv.includes("dev");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function checkShellAvailable(shell) {
|
|
97
|
+
const cmds = { bash: "bash --version", cmd: "cmd /c echo ok", pwsh: "pwsh -NoProfile -c echo ok" };
|
|
98
|
+
try {
|
|
99
|
+
execSync(cmds[shell], { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "ignore"] });
|
|
100
|
+
return true;
|
|
101
|
+
} catch { return false; }
|
|
102
|
+
}
|
|
103
|
+
|
|
105
104
|
function getVersion(filePath) {
|
|
106
105
|
try {
|
|
107
106
|
const content = readFileSync(filePath, "utf8");
|
|
108
107
|
const match = content.match(/VERSION\s*=\s*"([^"]+)"/);
|
|
109
|
-
return match ? match[1] : null;
|
|
108
|
+
return match ? match[1] : null;
|
|
110
109
|
} catch { return null; }
|
|
111
110
|
}
|
|
112
111
|
|
|
@@ -232,573 +231,573 @@ async function cleanupStaleTeamSessions(staleSessions) {
|
|
|
232
231
|
|
|
233
232
|
return { cleaned, failed };
|
|
234
233
|
}
|
|
235
|
-
|
|
236
|
-
function escapeRegExp(value) {
|
|
237
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function hasProfileSection(tomlContent, profileName) {
|
|
241
|
-
const section = `^\\[profiles\\.${escapeRegExp(profileName)}\\]\\s*$`;
|
|
242
|
-
return new RegExp(section, "m").test(tomlContent);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function ensureCodexProfiles() {
|
|
246
|
-
try {
|
|
247
|
-
if (!existsSync(CODEX_DIR)) mkdirSync(CODEX_DIR, { recursive: true });
|
|
248
|
-
|
|
249
|
-
const original = existsSync(CODEX_CONFIG_PATH)
|
|
250
|
-
? readFileSync(CODEX_CONFIG_PATH, "utf8")
|
|
251
|
-
: "";
|
|
252
|
-
|
|
253
|
-
let updated = original;
|
|
254
|
-
let added = 0;
|
|
255
|
-
|
|
256
|
-
for (const profile of REQUIRED_CODEX_PROFILES) {
|
|
257
|
-
if (hasProfileSection(updated, profile.name)) continue;
|
|
258
|
-
|
|
259
|
-
if (updated.length > 0 && !updated.endsWith("\n")) updated += "\n";
|
|
260
|
-
if (updated.trim().length > 0) updated += "\n";
|
|
261
|
-
updated += `[profiles.${profile.name}]\n${profile.lines.join("\n")}\n`;
|
|
262
|
-
added++;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (added > 0) {
|
|
266
|
-
writeFileSync(CODEX_CONFIG_PATH, updated, "utf8");
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return { ok: true, added };
|
|
270
|
-
} catch (e) {
|
|
271
|
-
return { ok: false, added: 0, message: e.message };
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function syncFile(src, dst, label) {
|
|
276
|
-
const dstDir = dirname(dst);
|
|
277
|
-
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
278
|
-
|
|
279
|
-
if (!existsSync(src)) {
|
|
280
|
-
fail(`${label}: 소스 파일 없음 (${src})`);
|
|
281
|
-
return false;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const srcVer = getVersion(src);
|
|
285
|
-
const dstVer = existsSync(dst) ? getVersion(dst) : null;
|
|
286
|
-
|
|
287
|
-
if (!existsSync(dst)) {
|
|
288
|
-
copyFileSync(src, dst);
|
|
289
|
-
try { chmodSync(dst, 0o755); } catch {}
|
|
290
|
-
ok(`${label}: 설치됨 ${srcVer ? `(v${srcVer})` : ""}`);
|
|
291
|
-
return true;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const srcContent = readFileSync(src, "utf8");
|
|
295
|
-
const dstContent = readFileSync(dst, "utf8");
|
|
296
|
-
if (srcContent !== dstContent) {
|
|
297
|
-
copyFileSync(src, dst);
|
|
298
|
-
try { chmodSync(dst, 0o755); } catch {}
|
|
299
|
-
const verInfo = (srcVer && dstVer && srcVer !== dstVer)
|
|
300
|
-
? `(v${dstVer} → v${srcVer})`
|
|
301
|
-
: srcVer ? `(v${srcVer}, 내용 변경)` : "(내용 변경)";
|
|
302
|
-
ok(`${label}: 업데이트됨 ${verInfo}`);
|
|
303
|
-
return true;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
ok(`${label}: 최신 상태 ${srcVer ? `(v${srcVer})` : ""}`);
|
|
307
|
-
return false;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// ── 크로스 셸 진단 ──
|
|
311
|
-
|
|
312
|
-
function checkCliCrossShell(cmd, installHint) {
|
|
313
|
-
const shells = process.platform === "win32" ? ["bash", "cmd", "pwsh"] : ["bash"];
|
|
314
|
-
let anyFound = false;
|
|
315
|
-
let bashMissing = false;
|
|
316
|
-
|
|
317
|
-
for (const shell of shells) {
|
|
318
|
-
if (!checkShellAvailable(shell)) {
|
|
319
|
-
info(`${shell}: ${DIM}셸 없음 (건너뜀)${RESET}`);
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
const p = whichInShell(cmd, shell);
|
|
323
|
-
if (p) {
|
|
324
|
-
ok(`${shell}: ${p}`);
|
|
325
|
-
anyFound = true;
|
|
326
|
-
} else {
|
|
327
|
-
fail(`${shell}: 미발견`);
|
|
328
|
-
if (shell === "bash") bashMissing = true;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (!anyFound) {
|
|
333
|
-
info(`미설치 (선택사항) — ${installHint}`);
|
|
334
|
-
info("없으면 Claude 네이티브 에이전트로 fallback");
|
|
335
|
-
return 1;
|
|
336
|
-
}
|
|
337
|
-
if (bashMissing) {
|
|
338
|
-
warn("bash에서 미발견 — tfx-route.sh 실행 불가");
|
|
339
|
-
info('→ ~/.bashrc에 추가: export PATH="$PATH:$APPDATA/npm"');
|
|
340
|
-
return 1;
|
|
341
|
-
}
|
|
342
|
-
return 0;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// ── 명령어 ──
|
|
346
|
-
|
|
347
|
-
function cmdSetup() {
|
|
348
|
-
console.log(`\n${BOLD}triflux setup${RESET}\n`);
|
|
349
|
-
|
|
350
|
-
syncFile(
|
|
351
|
-
join(PKG_ROOT, "scripts", "tfx-route.sh"),
|
|
352
|
-
join(CLAUDE_DIR, "scripts", "tfx-route.sh"),
|
|
353
|
-
"tfx-route.sh"
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
syncFile(
|
|
357
|
-
join(PKG_ROOT, "hud", "hud-qos-status.mjs"),
|
|
358
|
-
join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"),
|
|
359
|
-
"hud-qos-status.mjs"
|
|
360
|
-
);
|
|
361
|
-
|
|
362
|
-
syncFile(
|
|
363
|
-
join(PKG_ROOT, "scripts", "notion-read.mjs"),
|
|
364
|
-
join(CLAUDE_DIR, "scripts", "notion-read.mjs"),
|
|
365
|
-
"notion-read.mjs"
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
syncFile(
|
|
369
|
-
join(PKG_ROOT, "scripts", "tfx-route-post.mjs"),
|
|
370
|
-
join(CLAUDE_DIR, "scripts", "tfx-route-post.mjs"),
|
|
371
|
-
"tfx-route-post.mjs"
|
|
372
|
-
);
|
|
373
|
-
|
|
374
|
-
syncFile(
|
|
375
|
-
join(PKG_ROOT, "scripts", "tfx-batch-stats.mjs"),
|
|
376
|
-
join(CLAUDE_DIR, "scripts", "tfx-batch-stats.mjs"),
|
|
377
|
-
"tfx-batch-stats.mjs"
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
// 스킬 동기화 (~/.claude/skills/{name}/SKILL.md)
|
|
381
|
-
const skillsSrc = join(PKG_ROOT, "skills");
|
|
382
|
-
const skillsDst = join(CLAUDE_DIR, "skills");
|
|
383
|
-
if (existsSync(skillsSrc)) {
|
|
384
|
-
let skillCount = 0;
|
|
385
|
-
let skillTotal = 0;
|
|
386
|
-
for (const name of readdirSync(skillsSrc)) {
|
|
387
|
-
const src = join(skillsSrc, name, "SKILL.md");
|
|
388
|
-
const dst = join(skillsDst, name, "SKILL.md");
|
|
389
|
-
if (!existsSync(src)) continue;
|
|
390
|
-
skillTotal++;
|
|
391
|
-
|
|
392
|
-
const dstDir = dirname(dst);
|
|
393
|
-
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
394
|
-
|
|
395
|
-
if (!existsSync(dst)) {
|
|
396
|
-
copyFileSync(src, dst);
|
|
397
|
-
skillCount++;
|
|
398
|
-
} else {
|
|
399
|
-
const srcContent = readFileSync(src, "utf8");
|
|
400
|
-
const dstContent = readFileSync(dst, "utf8");
|
|
401
|
-
if (srcContent !== dstContent) {
|
|
402
|
-
copyFileSync(src, dst);
|
|
403
|
-
skillCount++;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
if (skillCount > 0) {
|
|
408
|
-
ok(`스킬: ${skillCount}/${skillTotal}개 업데이트됨`);
|
|
409
|
-
} else {
|
|
410
|
-
ok(`스킬: ${skillTotal}개 최신 상태`);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const codexProfileResult = ensureCodexProfiles();
|
|
415
|
-
if (!codexProfileResult.ok) {
|
|
416
|
-
warn(`Codex profiles 설정 실패: ${codexProfileResult.message}`);
|
|
417
|
-
} else if (codexProfileResult.added > 0) {
|
|
418
|
-
ok(`Codex profiles: ${codexProfileResult.added}개 추가됨 (~/.codex/config.toml)`);
|
|
419
|
-
} else {
|
|
420
|
-
ok("Codex profiles: 이미 준비됨");
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// hub MCP 사전 등록 (서버 미실행이어도 설정만 등록 — hub start 시 즉시 사용 가능)
|
|
424
|
-
if (existsSync(join(PKG_ROOT, "hub", "server.mjs"))) {
|
|
425
|
-
const defaultHubUrl = `http://127.0.0.1:${process.env.TFX_HUB_PORT || "27888"}/mcp`;
|
|
426
|
-
autoRegisterMcp(defaultHubUrl);
|
|
427
|
-
console.log("");
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
// HUD statusLine 설정
|
|
431
|
-
console.log(`${CYAN}[HUD 설정]${RESET}`);
|
|
432
|
-
const settingsPath = join(CLAUDE_DIR, "settings.json");
|
|
433
|
-
const hudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
434
|
-
|
|
435
|
-
if (existsSync(hudPath)) {
|
|
436
|
-
try {
|
|
437
|
-
let settings = {};
|
|
438
|
-
if (existsSync(settingsPath)) {
|
|
439
|
-
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const currentCmd = settings.statusLine?.command || "";
|
|
443
|
-
if (currentCmd.includes("hud-qos-status.mjs")) {
|
|
444
|
-
ok("statusLine 이미 설정됨");
|
|
445
|
-
} else {
|
|
446
|
-
const nodePath = process.execPath.replace(/\\/g, "/");
|
|
447
|
-
const hudForward = hudPath.replace(/\\/g, "/");
|
|
448
|
-
const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
|
|
449
|
-
const hudRef = hudForward.includes(" ") ? `"${hudForward}"` : hudForward;
|
|
450
|
-
|
|
451
|
-
if (currentCmd) {
|
|
452
|
-
warn(`기존 statusLine 덮어쓰기: ${currentCmd}`);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
settings.statusLine = {
|
|
456
|
-
type: "command",
|
|
457
|
-
command: `${nodeRef} ${hudRef}`,
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
461
|
-
ok("statusLine 설정 완료 — 세션 재시작 후 HUD 표시");
|
|
462
|
-
}
|
|
463
|
-
} catch (e) {
|
|
464
|
-
fail(`settings.json 처리 실패: ${e.message}`);
|
|
465
|
-
}
|
|
466
|
-
} else {
|
|
467
|
-
warn("HUD 파일 없음 — 먼저 파일 동기화 필요");
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
console.log(`\n${DIM}설치 위치: ${CLAUDE_DIR}${RESET}\n`);
|
|
471
|
-
}
|
|
472
|
-
|
|
234
|
+
|
|
235
|
+
function escapeRegExp(value) {
|
|
236
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function hasProfileSection(tomlContent, profileName) {
|
|
240
|
+
const section = `^\\[profiles\\.${escapeRegExp(profileName)}\\]\\s*$`;
|
|
241
|
+
return new RegExp(section, "m").test(tomlContent);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function ensureCodexProfiles() {
|
|
245
|
+
try {
|
|
246
|
+
if (!existsSync(CODEX_DIR)) mkdirSync(CODEX_DIR, { recursive: true });
|
|
247
|
+
|
|
248
|
+
const original = existsSync(CODEX_CONFIG_PATH)
|
|
249
|
+
? readFileSync(CODEX_CONFIG_PATH, "utf8")
|
|
250
|
+
: "";
|
|
251
|
+
|
|
252
|
+
let updated = original;
|
|
253
|
+
let added = 0;
|
|
254
|
+
|
|
255
|
+
for (const profile of REQUIRED_CODEX_PROFILES) {
|
|
256
|
+
if (hasProfileSection(updated, profile.name)) continue;
|
|
257
|
+
|
|
258
|
+
if (updated.length > 0 && !updated.endsWith("\n")) updated += "\n";
|
|
259
|
+
if (updated.trim().length > 0) updated += "\n";
|
|
260
|
+
updated += `[profiles.${profile.name}]\n${profile.lines.join("\n")}\n`;
|
|
261
|
+
added++;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (added > 0) {
|
|
265
|
+
writeFileSync(CODEX_CONFIG_PATH, updated, "utf8");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { ok: true, added };
|
|
269
|
+
} catch (e) {
|
|
270
|
+
return { ok: false, added: 0, message: e.message };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function syncFile(src, dst, label) {
|
|
275
|
+
const dstDir = dirname(dst);
|
|
276
|
+
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
277
|
+
|
|
278
|
+
if (!existsSync(src)) {
|
|
279
|
+
fail(`${label}: 소스 파일 없음 (${src})`);
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const srcVer = getVersion(src);
|
|
284
|
+
const dstVer = existsSync(dst) ? getVersion(dst) : null;
|
|
285
|
+
|
|
286
|
+
if (!existsSync(dst)) {
|
|
287
|
+
copyFileSync(src, dst);
|
|
288
|
+
try { chmodSync(dst, 0o755); } catch {}
|
|
289
|
+
ok(`${label}: 설치됨 ${srcVer ? `(v${srcVer})` : ""}`);
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const srcContent = readFileSync(src, "utf8");
|
|
294
|
+
const dstContent = readFileSync(dst, "utf8");
|
|
295
|
+
if (srcContent !== dstContent) {
|
|
296
|
+
copyFileSync(src, dst);
|
|
297
|
+
try { chmodSync(dst, 0o755); } catch {}
|
|
298
|
+
const verInfo = (srcVer && dstVer && srcVer !== dstVer)
|
|
299
|
+
? `(v${dstVer} → v${srcVer})`
|
|
300
|
+
: srcVer ? `(v${srcVer}, 내용 변경)` : "(내용 변경)";
|
|
301
|
+
ok(`${label}: 업데이트됨 ${verInfo}`);
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
ok(`${label}: 최신 상태 ${srcVer ? `(v${srcVer})` : ""}`);
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── 크로스 셸 진단 ──
|
|
310
|
+
|
|
311
|
+
function checkCliCrossShell(cmd, installHint) {
|
|
312
|
+
const shells = process.platform === "win32" ? ["bash", "cmd", "pwsh"] : ["bash"];
|
|
313
|
+
let anyFound = false;
|
|
314
|
+
let bashMissing = false;
|
|
315
|
+
|
|
316
|
+
for (const shell of shells) {
|
|
317
|
+
if (!checkShellAvailable(shell)) {
|
|
318
|
+
info(`${shell}: ${DIM}셸 없음 (건너뜀)${RESET}`);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const p = whichInShell(cmd, shell);
|
|
322
|
+
if (p) {
|
|
323
|
+
ok(`${shell}: ${p}`);
|
|
324
|
+
anyFound = true;
|
|
325
|
+
} else {
|
|
326
|
+
fail(`${shell}: 미발견`);
|
|
327
|
+
if (shell === "bash") bashMissing = true;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!anyFound) {
|
|
332
|
+
info(`미설치 (선택사항) — ${installHint}`);
|
|
333
|
+
info("없으면 Claude 네이티브 에이전트로 fallback");
|
|
334
|
+
return 1;
|
|
335
|
+
}
|
|
336
|
+
if (bashMissing) {
|
|
337
|
+
warn("bash에서 미발견 — tfx-route.sh 실행 불가");
|
|
338
|
+
info('→ ~/.bashrc에 추가: export PATH="$PATH:$APPDATA/npm"');
|
|
339
|
+
return 1;
|
|
340
|
+
}
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── 명령어 ──
|
|
345
|
+
|
|
346
|
+
function cmdSetup() {
|
|
347
|
+
console.log(`\n${BOLD}triflux setup${RESET}\n`);
|
|
348
|
+
|
|
349
|
+
syncFile(
|
|
350
|
+
join(PKG_ROOT, "scripts", "tfx-route.sh"),
|
|
351
|
+
join(CLAUDE_DIR, "scripts", "tfx-route.sh"),
|
|
352
|
+
"tfx-route.sh"
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
syncFile(
|
|
356
|
+
join(PKG_ROOT, "hud", "hud-qos-status.mjs"),
|
|
357
|
+
join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"),
|
|
358
|
+
"hud-qos-status.mjs"
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
syncFile(
|
|
362
|
+
join(PKG_ROOT, "scripts", "notion-read.mjs"),
|
|
363
|
+
join(CLAUDE_DIR, "scripts", "notion-read.mjs"),
|
|
364
|
+
"notion-read.mjs"
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
syncFile(
|
|
368
|
+
join(PKG_ROOT, "scripts", "tfx-route-post.mjs"),
|
|
369
|
+
join(CLAUDE_DIR, "scripts", "tfx-route-post.mjs"),
|
|
370
|
+
"tfx-route-post.mjs"
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
syncFile(
|
|
374
|
+
join(PKG_ROOT, "scripts", "tfx-batch-stats.mjs"),
|
|
375
|
+
join(CLAUDE_DIR, "scripts", "tfx-batch-stats.mjs"),
|
|
376
|
+
"tfx-batch-stats.mjs"
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
// 스킬 동기화 (~/.claude/skills/{name}/SKILL.md)
|
|
380
|
+
const skillsSrc = join(PKG_ROOT, "skills");
|
|
381
|
+
const skillsDst = join(CLAUDE_DIR, "skills");
|
|
382
|
+
if (existsSync(skillsSrc)) {
|
|
383
|
+
let skillCount = 0;
|
|
384
|
+
let skillTotal = 0;
|
|
385
|
+
for (const name of readdirSync(skillsSrc)) {
|
|
386
|
+
const src = join(skillsSrc, name, "SKILL.md");
|
|
387
|
+
const dst = join(skillsDst, name, "SKILL.md");
|
|
388
|
+
if (!existsSync(src)) continue;
|
|
389
|
+
skillTotal++;
|
|
390
|
+
|
|
391
|
+
const dstDir = dirname(dst);
|
|
392
|
+
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
393
|
+
|
|
394
|
+
if (!existsSync(dst)) {
|
|
395
|
+
copyFileSync(src, dst);
|
|
396
|
+
skillCount++;
|
|
397
|
+
} else {
|
|
398
|
+
const srcContent = readFileSync(src, "utf8");
|
|
399
|
+
const dstContent = readFileSync(dst, "utf8");
|
|
400
|
+
if (srcContent !== dstContent) {
|
|
401
|
+
copyFileSync(src, dst);
|
|
402
|
+
skillCount++;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (skillCount > 0) {
|
|
407
|
+
ok(`스킬: ${skillCount}/${skillTotal}개 업데이트됨`);
|
|
408
|
+
} else {
|
|
409
|
+
ok(`스킬: ${skillTotal}개 최신 상태`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const codexProfileResult = ensureCodexProfiles();
|
|
414
|
+
if (!codexProfileResult.ok) {
|
|
415
|
+
warn(`Codex profiles 설정 실패: ${codexProfileResult.message}`);
|
|
416
|
+
} else if (codexProfileResult.added > 0) {
|
|
417
|
+
ok(`Codex profiles: ${codexProfileResult.added}개 추가됨 (~/.codex/config.toml)`);
|
|
418
|
+
} else {
|
|
419
|
+
ok("Codex profiles: 이미 준비됨");
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// hub MCP 사전 등록 (서버 미실행이어도 설정만 등록 — hub start 시 즉시 사용 가능)
|
|
423
|
+
if (existsSync(join(PKG_ROOT, "hub", "server.mjs"))) {
|
|
424
|
+
const defaultHubUrl = `http://127.0.0.1:${process.env.TFX_HUB_PORT || "27888"}/mcp`;
|
|
425
|
+
autoRegisterMcp(defaultHubUrl);
|
|
426
|
+
console.log("");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// HUD statusLine 설정
|
|
430
|
+
console.log(`${CYAN}[HUD 설정]${RESET}`);
|
|
431
|
+
const settingsPath = join(CLAUDE_DIR, "settings.json");
|
|
432
|
+
const hudPath = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
433
|
+
|
|
434
|
+
if (existsSync(hudPath)) {
|
|
435
|
+
try {
|
|
436
|
+
let settings = {};
|
|
437
|
+
if (existsSync(settingsPath)) {
|
|
438
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const currentCmd = settings.statusLine?.command || "";
|
|
442
|
+
if (currentCmd.includes("hud-qos-status.mjs")) {
|
|
443
|
+
ok("statusLine 이미 설정됨");
|
|
444
|
+
} else {
|
|
445
|
+
const nodePath = process.execPath.replace(/\\/g, "/");
|
|
446
|
+
const hudForward = hudPath.replace(/\\/g, "/");
|
|
447
|
+
const nodeRef = nodePath.includes(" ") ? `"${nodePath}"` : nodePath;
|
|
448
|
+
const hudRef = hudForward.includes(" ") ? `"${hudForward}"` : hudForward;
|
|
449
|
+
|
|
450
|
+
if (currentCmd) {
|
|
451
|
+
warn(`기존 statusLine 덮어쓰기: ${currentCmd}`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
settings.statusLine = {
|
|
455
|
+
type: "command",
|
|
456
|
+
command: `${nodeRef} ${hudRef}`,
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
460
|
+
ok("statusLine 설정 완료 — 세션 재시작 후 HUD 표시");
|
|
461
|
+
}
|
|
462
|
+
} catch (e) {
|
|
463
|
+
fail(`settings.json 처리 실패: ${e.message}`);
|
|
464
|
+
}
|
|
465
|
+
} else {
|
|
466
|
+
warn("HUD 파일 없음 — 먼저 파일 동기화 필요");
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
console.log(`\n${DIM}설치 위치: ${CLAUDE_DIR}${RESET}\n`);
|
|
470
|
+
}
|
|
471
|
+
|
|
473
472
|
async function cmdDoctor(options = {}) {
|
|
474
473
|
const { fix = false, reset = false } = options;
|
|
475
474
|
const modeLabel = reset ? ` ${RED}--reset${RESET}` : fix ? ` ${YELLOW}--fix${RESET}` : "";
|
|
476
475
|
console.log(`\n ${AMBER}${BOLD}⬡ triflux doctor${RESET} ${VER}${modeLabel}\n`);
|
|
477
|
-
console.log(` ${LINE}`);
|
|
478
|
-
|
|
479
|
-
// ── reset 모드: 캐시 전체 초기화 ──
|
|
480
|
-
if (reset) {
|
|
481
|
-
section("Cache Reset");
|
|
482
|
-
const cacheDir = join(CLAUDE_DIR, "cache");
|
|
483
|
-
const resetFiles = [
|
|
484
|
-
"claude-usage-cache.json",
|
|
485
|
-
".claude-refresh-lock",
|
|
486
|
-
"codex-rate-limits-cache.json",
|
|
487
|
-
"gemini-quota-cache.json",
|
|
488
|
-
"gemini-project-id.json",
|
|
489
|
-
"gemini-session-cache.json",
|
|
490
|
-
"gemini-rpm-tracker.json",
|
|
491
|
-
"sv-accumulator.json",
|
|
492
|
-
"mcp-inventory.json",
|
|
493
|
-
"cli-issues.jsonl",
|
|
494
|
-
"triflux-update-check.json",
|
|
495
|
-
];
|
|
496
|
-
let cleared = 0;
|
|
497
|
-
for (const name of resetFiles) {
|
|
498
|
-
const fp = join(cacheDir, name);
|
|
499
|
-
if (existsSync(fp)) {
|
|
500
|
-
try { unlinkSync(fp); cleared++; ok(`삭제됨: ${name}`); }
|
|
501
|
-
catch (e) { fail(`삭제 실패: ${name} — ${e.message}`); }
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
if (cleared === 0) {
|
|
505
|
-
ok("삭제할 캐시 파일 없음 (이미 깨끗함)");
|
|
506
|
-
} else {
|
|
507
|
-
console.log("");
|
|
508
|
-
ok(`${BOLD}${cleared}개${RESET} 캐시 파일 초기화 완료`);
|
|
509
|
-
}
|
|
510
|
-
// 캐시 즉시 재생성
|
|
511
|
-
console.log("");
|
|
512
|
-
section("Cache Rebuild");
|
|
513
|
-
const mcpCheck = join(PKG_ROOT, "scripts", "mcp-check.mjs");
|
|
514
|
-
if (existsSync(mcpCheck)) {
|
|
515
|
-
try {
|
|
516
|
-
|
|
517
|
-
ok("MCP 인벤토리 재생성됨");
|
|
518
|
-
} catch { warn("MCP 인벤토리 재생성 실패 — 다음 세션에서 자동 재시도"); }
|
|
519
|
-
}
|
|
520
|
-
const hudScript = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
521
|
-
if (existsSync(hudScript)) {
|
|
522
|
-
try {
|
|
523
|
-
|
|
524
|
-
ok("Claude 사용량 캐시 재생성됨");
|
|
525
|
-
} catch { warn("Claude 사용량 캐시 재생성 실패 — 다음 API 호출 시 자동 생성"); }
|
|
526
|
-
try {
|
|
527
|
-
|
|
528
|
-
ok("Codex 레이트 리밋 캐시 재생성됨");
|
|
529
|
-
} catch { warn("Codex 레이트 리밋 캐시 재생성 실패"); }
|
|
530
|
-
try {
|
|
531
|
-
|
|
532
|
-
ok("Gemini 쿼터 캐시 재생성됨");
|
|
533
|
-
} catch { warn("Gemini 쿼터 캐시 재생성 실패"); }
|
|
534
|
-
}
|
|
535
|
-
console.log(`\n ${LINE}`);
|
|
536
|
-
console.log(` ${GREEN_BRIGHT}${BOLD}✓ 캐시 초기화 + 재생성 완료${RESET}\n`);
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// ── fix 모드: 파일 동기화 + 캐시 정리 후 진단 ──
|
|
541
|
-
if (fix) {
|
|
542
|
-
section("Auto Fix");
|
|
543
|
-
syncFile(
|
|
544
|
-
join(PKG_ROOT, "scripts", "tfx-route.sh"),
|
|
545
|
-
join(CLAUDE_DIR, "scripts", "tfx-route.sh"),
|
|
546
|
-
"tfx-route.sh"
|
|
547
|
-
);
|
|
548
|
-
syncFile(
|
|
549
|
-
join(PKG_ROOT, "hud", "hud-qos-status.mjs"),
|
|
550
|
-
join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"),
|
|
551
|
-
"hud-qos-status.mjs"
|
|
552
|
-
);
|
|
553
|
-
syncFile(
|
|
554
|
-
join(PKG_ROOT, "scripts", "notion-read.mjs"),
|
|
555
|
-
join(CLAUDE_DIR, "scripts", "notion-read.mjs"),
|
|
556
|
-
"notion-read.mjs"
|
|
557
|
-
);
|
|
558
|
-
// 스킬 동기화
|
|
559
|
-
const fSkillsSrc = join(PKG_ROOT, "skills");
|
|
560
|
-
const fSkillsDst = join(CLAUDE_DIR, "skills");
|
|
561
|
-
if (existsSync(fSkillsSrc)) {
|
|
562
|
-
let sc = 0, st = 0;
|
|
563
|
-
for (const name of readdirSync(fSkillsSrc)) {
|
|
564
|
-
const src = join(fSkillsSrc, name, "SKILL.md");
|
|
565
|
-
const dst = join(fSkillsDst, name, "SKILL.md");
|
|
566
|
-
if (!existsSync(src)) continue;
|
|
567
|
-
st++;
|
|
568
|
-
const dstDir = dirname(dst);
|
|
569
|
-
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
570
|
-
if (!existsSync(dst)) { copyFileSync(src, dst); sc++; }
|
|
571
|
-
else if (readFileSync(src, "utf8") !== readFileSync(dst, "utf8")) { copyFileSync(src, dst); sc++; }
|
|
572
|
-
}
|
|
573
|
-
if (sc > 0) ok(`스킬: ${sc}/${st}개 업데이트됨`);
|
|
574
|
-
else ok(`스킬: ${st}개 최신 상태`);
|
|
575
|
-
}
|
|
576
|
-
const profileFix = ensureCodexProfiles();
|
|
577
|
-
if (!profileFix.ok) {
|
|
578
|
-
warn(`Codex Profiles 자동 복구 실패: ${profileFix.message}`);
|
|
579
|
-
} else if (profileFix.added > 0) {
|
|
580
|
-
ok(`Codex Profiles: ${profileFix.added}개 추가됨`);
|
|
581
|
-
} else {
|
|
582
|
-
info("Codex Profiles: 이미 최신 상태");
|
|
583
|
-
}
|
|
584
|
-
// 에러/스테일 캐시 정리
|
|
585
|
-
const fCacheDir = join(CLAUDE_DIR, "cache");
|
|
586
|
-
const staleNames = ["claude-usage-cache.json", ".claude-refresh-lock", "codex-rate-limits-cache.json"];
|
|
587
|
-
let cleaned = 0;
|
|
588
|
-
for (const name of staleNames) {
|
|
589
|
-
const fp = join(fCacheDir, name);
|
|
590
|
-
if (!existsSync(fp)) continue;
|
|
591
|
-
try {
|
|
592
|
-
const parsed = JSON.parse(readFileSync(fp, "utf8"));
|
|
593
|
-
if (parsed.error || name.startsWith(".")) { unlinkSync(fp); cleaned++; ok(`에러 캐시 정리: ${name}`); }
|
|
594
|
-
} catch { try { unlinkSync(fp); cleaned++; ok(`손상된 캐시 정리: ${name}`); } catch {} }
|
|
595
|
-
}
|
|
596
|
-
if (cleaned === 0) info("에러 캐시 없음");
|
|
597
|
-
console.log(`\n ${LINE}`);
|
|
598
|
-
info("수정 완료 — 아래 진단 결과를 확인하세요");
|
|
599
|
-
console.log("");
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
let issues = 0;
|
|
603
|
-
|
|
604
|
-
// 1. tfx-route.sh
|
|
605
|
-
section("tfx-route.sh");
|
|
606
|
-
const routeSh = join(CLAUDE_DIR, "scripts", "tfx-route.sh");
|
|
607
|
-
if (existsSync(routeSh)) {
|
|
608
|
-
const ver = getVersion(routeSh);
|
|
609
|
-
ok(`설치됨 ${ver ? `${DIM}v${ver}${RESET}` : ""}`);
|
|
610
|
-
} else {
|
|
611
|
-
fail("미설치 — tfx setup 실행 필요");
|
|
612
|
-
issues++;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// 2. HUD
|
|
616
|
-
section("HUD");
|
|
617
|
-
const hud = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
618
|
-
if (existsSync(hud)) {
|
|
619
|
-
ok("설치됨");
|
|
620
|
-
} else {
|
|
621
|
-
warn("미설치 ${GRAY}(선택사항)${RESET}");
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// 3. Codex CLI
|
|
625
|
-
section(`Codex CLI ${WHITE_BRIGHT}●${RESET}`);
|
|
626
|
-
issues += checkCliCrossShell("codex", "npm install -g @openai/codex");
|
|
627
|
-
if (which("codex")) {
|
|
628
|
-
if (process.env.OPENAI_API_KEY) {
|
|
629
|
-
ok("OPENAI_API_KEY 설정됨");
|
|
630
|
-
} else {
|
|
631
|
-
warn(`OPENAI_API_KEY 미설정 ${GRAY}(Pro 구독이면 불필요)${RESET}`);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// 4. Codex Profiles
|
|
636
|
-
section("Codex Profiles");
|
|
637
|
-
if (existsSync(CODEX_CONFIG_PATH)) {
|
|
638
|
-
const codexConfig = readFileSync(CODEX_CONFIG_PATH, "utf8");
|
|
639
|
-
for (const profile of REQUIRED_CODEX_PROFILES) {
|
|
640
|
-
if (hasProfileSection(codexConfig, profile.name)) {
|
|
641
|
-
ok(`${profile.name}: 정상`);
|
|
642
|
-
} else {
|
|
643
|
-
warn(`${profile.name}: 미설정`);
|
|
644
|
-
issues++;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
} else {
|
|
648
|
-
warn("config.toml 미존재");
|
|
649
|
-
issues++;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// 5. Gemini CLI
|
|
653
|
-
section(`Gemini CLI ${BLUE}●${RESET}`);
|
|
654
|
-
issues += checkCliCrossShell("gemini", "npm install -g @google/gemini-cli");
|
|
655
|
-
if (which("gemini")) {
|
|
656
|
-
if (process.env.GEMINI_API_KEY) {
|
|
657
|
-
ok("GEMINI_API_KEY 설정됨");
|
|
658
|
-
} else {
|
|
659
|
-
warn(`GEMINI_API_KEY 미설정 ${GRAY}(gemini auth login)${RESET}`);
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// 6. Claude Code
|
|
664
|
-
section(`Claude Code ${AMBER}●${RESET}`);
|
|
665
|
-
const claudePath = which("claude");
|
|
666
|
-
if (claudePath) {
|
|
667
|
-
ok("설치됨");
|
|
668
|
-
} else {
|
|
669
|
-
fail("미설치 (필수)");
|
|
670
|
-
issues++;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// 7. 스킬 설치 상태
|
|
674
|
-
section("Skills");
|
|
675
|
-
const skillsSrc = join(PKG_ROOT, "skills");
|
|
676
|
-
const skillsDst = join(CLAUDE_DIR, "skills");
|
|
677
|
-
if (existsSync(skillsSrc)) {
|
|
678
|
-
let installed = 0;
|
|
679
|
-
let total = 0;
|
|
680
|
-
const missing = [];
|
|
681
|
-
for (const name of readdirSync(skillsSrc)) {
|
|
682
|
-
if (!existsSync(join(skillsSrc, name, "SKILL.md"))) continue;
|
|
683
|
-
total++;
|
|
684
|
-
if (existsSync(join(skillsDst, name, "SKILL.md"))) {
|
|
685
|
-
installed++;
|
|
686
|
-
} else {
|
|
687
|
-
missing.push(name);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
if (installed === total) {
|
|
691
|
-
ok(`${installed}/${total}개 설치됨`);
|
|
692
|
-
} else {
|
|
693
|
-
warn(`${installed}/${total}개 설치됨 — 미설치: ${missing.join(", ")}`);
|
|
694
|
-
info("triflux setup으로 동기화 가능");
|
|
695
|
-
issues++;
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
// 8. 플러그인 등록
|
|
700
|
-
section("Plugin");
|
|
701
|
-
const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
|
|
702
|
-
if (existsSync(pluginsFile)) {
|
|
703
|
-
const content = readFileSync(pluginsFile, "utf8");
|
|
704
|
-
if (content.includes("triflux")) {
|
|
705
|
-
ok("triflux 플러그인 등록됨");
|
|
706
|
-
} else {
|
|
707
|
-
warn("triflux 플러그인 미등록 — npm 단독 사용 중");
|
|
708
|
-
info("플러그인 등록: /plugin marketplace add <repo-url>");
|
|
709
|
-
}
|
|
710
|
-
} else {
|
|
711
|
-
info("플러그인 시스템 감지 안 됨 — npm 단독 사용");
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// 9. MCP 인벤토리
|
|
715
|
-
section("MCP Inventory");
|
|
716
|
-
const mcpCache = join(CLAUDE_DIR, "cache", "mcp-inventory.json");
|
|
717
|
-
if (existsSync(mcpCache)) {
|
|
718
|
-
try {
|
|
719
|
-
const inv = JSON.parse(readFileSync(mcpCache, "utf8"));
|
|
720
|
-
ok(`캐시 존재 (${inv.timestamp})`);
|
|
721
|
-
if (inv.codex?.servers?.length) {
|
|
722
|
-
const names = inv.codex.servers.map(s => s.name).join(", ");
|
|
723
|
-
info(`Codex: ${inv.codex.servers.length}개 서버 (${names})`);
|
|
724
|
-
}
|
|
725
|
-
if (inv.gemini?.servers?.length) {
|
|
726
|
-
const names = inv.gemini.servers.map(s => s.name).join(", ");
|
|
727
|
-
info(`Gemini: ${inv.gemini.servers.length}개 서버 (${names})`);
|
|
728
|
-
}
|
|
729
|
-
} catch {
|
|
730
|
-
warn("캐시 파일 파싱 실패");
|
|
731
|
-
}
|
|
732
|
-
} else {
|
|
733
|
-
warn("캐시 없음 — 다음 세션 시작 시 자동 생성");
|
|
734
|
-
info(`수동: node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}`);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// 10. CLI 이슈 트래커
|
|
738
|
-
section("CLI Issues");
|
|
739
|
-
const issuesFile = join(CLAUDE_DIR, "cache", "cli-issues.jsonl");
|
|
740
|
-
if (existsSync(issuesFile)) {
|
|
741
|
-
try {
|
|
742
|
-
const lines = readFileSync(issuesFile, "utf8").trim().split("\n").filter(Boolean);
|
|
743
|
-
const entries = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
744
|
-
const unresolved = entries.filter(e => !e.resolved);
|
|
745
|
-
|
|
746
|
-
if (unresolved.length === 0) {
|
|
747
|
-
ok("미해결 이슈 없음");
|
|
748
|
-
} else {
|
|
749
|
-
// 패턴별 그룹핑
|
|
750
|
-
const groups = {};
|
|
751
|
-
for (const e of unresolved) {
|
|
752
|
-
const key = `${e.cli}:${e.pattern}`;
|
|
753
|
-
if (!groups[key]) groups[key] = { ...e, count: 0 };
|
|
754
|
-
groups[key].count++;
|
|
755
|
-
if (e.ts > groups[key].ts) { groups[key].ts = e.ts; groups[key].snippet = e.snippet; }
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
// 알려진 해결 버전 (패턴별 수정된 triflux 버전)
|
|
759
|
-
const KNOWN_FIXES = {
|
|
760
|
-
"gemini:deprecated_flag": "1.8.9", // -p → --prompt
|
|
761
|
-
};
|
|
762
|
-
|
|
763
|
-
const currentVer = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8")).version;
|
|
764
|
-
let cleaned = 0;
|
|
765
|
-
|
|
766
|
-
for (const [key, g] of Object.entries(groups)) {
|
|
767
|
-
const fixVer = KNOWN_FIXES[key];
|
|
768
|
-
if (fixVer && currentVer >= fixVer) {
|
|
769
|
-
// 해결된 이슈 — 자동 정리
|
|
770
|
-
cleaned += g.count;
|
|
771
|
-
continue;
|
|
772
|
-
}
|
|
773
|
-
const age = Date.now() - g.ts;
|
|
774
|
-
const ago = age < 3600000 ? `${Math.round(age / 60000)}분 전` :
|
|
775
|
-
age < 86400000 ? `${Math.round(age / 3600000)}시간 전` :
|
|
776
|
-
`${Math.round(age / 86400000)}일 전`;
|
|
777
|
-
const sev = g.severity === "error" ? `${RED}ERROR${RESET}` : `${YELLOW}WARN${RESET}`;
|
|
778
|
-
warn(`[${sev}] ${g.cli}/${g.pattern} x${g.count} (최근: ${ago})`);
|
|
779
|
-
if (g.snippet) info(` ${g.snippet.substring(0, 120)}`);
|
|
780
|
-
if (fixVer) info(` 해결: triflux >= v${fixVer} (npm update -g triflux)`);
|
|
781
|
-
issues++;
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
// 해결된 이슈 자동 정리
|
|
785
|
-
if (cleaned > 0) {
|
|
786
|
-
const remaining = entries.filter(e => {
|
|
787
|
-
const key = `${e.cli}:${e.pattern}`;
|
|
788
|
-
const fixVer = KNOWN_FIXES[key];
|
|
789
|
-
return !(fixVer && currentVer >= fixVer);
|
|
790
|
-
});
|
|
791
|
-
writeFileSync(issuesFile, remaining.map(e => JSON.stringify(e)).join("\n") + (remaining.length ? "\n" : ""));
|
|
792
|
-
ok(`${cleaned}개 해결된 이슈 자동 정리됨`);
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
} catch (e) {
|
|
796
|
-
warn(`이슈 파일 읽기 실패: ${e.message}`);
|
|
797
|
-
}
|
|
798
|
-
} else {
|
|
799
|
-
ok("이슈 로그 없음 (정상)");
|
|
800
|
-
}
|
|
801
|
-
|
|
476
|
+
console.log(` ${LINE}`);
|
|
477
|
+
|
|
478
|
+
// ── reset 모드: 캐시 전체 초기화 ──
|
|
479
|
+
if (reset) {
|
|
480
|
+
section("Cache Reset");
|
|
481
|
+
const cacheDir = join(CLAUDE_DIR, "cache");
|
|
482
|
+
const resetFiles = [
|
|
483
|
+
"claude-usage-cache.json",
|
|
484
|
+
".claude-refresh-lock",
|
|
485
|
+
"codex-rate-limits-cache.json",
|
|
486
|
+
"gemini-quota-cache.json",
|
|
487
|
+
"gemini-project-id.json",
|
|
488
|
+
"gemini-session-cache.json",
|
|
489
|
+
"gemini-rpm-tracker.json",
|
|
490
|
+
"sv-accumulator.json",
|
|
491
|
+
"mcp-inventory.json",
|
|
492
|
+
"cli-issues.jsonl",
|
|
493
|
+
"triflux-update-check.json",
|
|
494
|
+
];
|
|
495
|
+
let cleared = 0;
|
|
496
|
+
for (const name of resetFiles) {
|
|
497
|
+
const fp = join(cacheDir, name);
|
|
498
|
+
if (existsSync(fp)) {
|
|
499
|
+
try { unlinkSync(fp); cleared++; ok(`삭제됨: ${name}`); }
|
|
500
|
+
catch (e) { fail(`삭제 실패: ${name} — ${e.message}`); }
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (cleared === 0) {
|
|
504
|
+
ok("삭제할 캐시 파일 없음 (이미 깨끗함)");
|
|
505
|
+
} else {
|
|
506
|
+
console.log("");
|
|
507
|
+
ok(`${BOLD}${cleared}개${RESET} 캐시 파일 초기화 완료`);
|
|
508
|
+
}
|
|
509
|
+
// 캐시 즉시 재생성
|
|
510
|
+
console.log("");
|
|
511
|
+
section("Cache Rebuild");
|
|
512
|
+
const mcpCheck = join(PKG_ROOT, "scripts", "mcp-check.mjs");
|
|
513
|
+
if (existsSync(mcpCheck)) {
|
|
514
|
+
try {
|
|
515
|
+
execFileSync(process.execPath, [mcpCheck], { timeout: 15000, stdio: "ignore" });
|
|
516
|
+
ok("MCP 인벤토리 재생성됨");
|
|
517
|
+
} catch { warn("MCP 인벤토리 재생성 실패 — 다음 세션에서 자동 재시도"); }
|
|
518
|
+
}
|
|
519
|
+
const hudScript = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
520
|
+
if (existsSync(hudScript)) {
|
|
521
|
+
try {
|
|
522
|
+
execFileSync(process.execPath, [hudScript, "--refresh-claude-usage"], { timeout: 20000, stdio: "ignore" });
|
|
523
|
+
ok("Claude 사용량 캐시 재생성됨");
|
|
524
|
+
} catch { warn("Claude 사용량 캐시 재생성 실패 — 다음 API 호출 시 자동 생성"); }
|
|
525
|
+
try {
|
|
526
|
+
execFileSync(process.execPath, [hudScript, "--refresh-codex-rate-limits"], { timeout: 15000, stdio: "ignore" });
|
|
527
|
+
ok("Codex 레이트 리밋 캐시 재생성됨");
|
|
528
|
+
} catch { warn("Codex 레이트 리밋 캐시 재생성 실패"); }
|
|
529
|
+
try {
|
|
530
|
+
execFileSync(process.execPath, [hudScript, "--refresh-gemini-quota"], { timeout: 15000, stdio: "ignore" });
|
|
531
|
+
ok("Gemini 쿼터 캐시 재생성됨");
|
|
532
|
+
} catch { warn("Gemini 쿼터 캐시 재생성 실패"); }
|
|
533
|
+
}
|
|
534
|
+
console.log(`\n ${LINE}`);
|
|
535
|
+
console.log(` ${GREEN_BRIGHT}${BOLD}✓ 캐시 초기화 + 재생성 완료${RESET}\n`);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ── fix 모드: 파일 동기화 + 캐시 정리 후 진단 ──
|
|
540
|
+
if (fix) {
|
|
541
|
+
section("Auto Fix");
|
|
542
|
+
syncFile(
|
|
543
|
+
join(PKG_ROOT, "scripts", "tfx-route.sh"),
|
|
544
|
+
join(CLAUDE_DIR, "scripts", "tfx-route.sh"),
|
|
545
|
+
"tfx-route.sh"
|
|
546
|
+
);
|
|
547
|
+
syncFile(
|
|
548
|
+
join(PKG_ROOT, "hud", "hud-qos-status.mjs"),
|
|
549
|
+
join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"),
|
|
550
|
+
"hud-qos-status.mjs"
|
|
551
|
+
);
|
|
552
|
+
syncFile(
|
|
553
|
+
join(PKG_ROOT, "scripts", "notion-read.mjs"),
|
|
554
|
+
join(CLAUDE_DIR, "scripts", "notion-read.mjs"),
|
|
555
|
+
"notion-read.mjs"
|
|
556
|
+
);
|
|
557
|
+
// 스킬 동기화
|
|
558
|
+
const fSkillsSrc = join(PKG_ROOT, "skills");
|
|
559
|
+
const fSkillsDst = join(CLAUDE_DIR, "skills");
|
|
560
|
+
if (existsSync(fSkillsSrc)) {
|
|
561
|
+
let sc = 0, st = 0;
|
|
562
|
+
for (const name of readdirSync(fSkillsSrc)) {
|
|
563
|
+
const src = join(fSkillsSrc, name, "SKILL.md");
|
|
564
|
+
const dst = join(fSkillsDst, name, "SKILL.md");
|
|
565
|
+
if (!existsSync(src)) continue;
|
|
566
|
+
st++;
|
|
567
|
+
const dstDir = dirname(dst);
|
|
568
|
+
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
569
|
+
if (!existsSync(dst)) { copyFileSync(src, dst); sc++; }
|
|
570
|
+
else if (readFileSync(src, "utf8") !== readFileSync(dst, "utf8")) { copyFileSync(src, dst); sc++; }
|
|
571
|
+
}
|
|
572
|
+
if (sc > 0) ok(`스킬: ${sc}/${st}개 업데이트됨`);
|
|
573
|
+
else ok(`스킬: ${st}개 최신 상태`);
|
|
574
|
+
}
|
|
575
|
+
const profileFix = ensureCodexProfiles();
|
|
576
|
+
if (!profileFix.ok) {
|
|
577
|
+
warn(`Codex Profiles 자동 복구 실패: ${profileFix.message}`);
|
|
578
|
+
} else if (profileFix.added > 0) {
|
|
579
|
+
ok(`Codex Profiles: ${profileFix.added}개 추가됨`);
|
|
580
|
+
} else {
|
|
581
|
+
info("Codex Profiles: 이미 최신 상태");
|
|
582
|
+
}
|
|
583
|
+
// 에러/스테일 캐시 정리
|
|
584
|
+
const fCacheDir = join(CLAUDE_DIR, "cache");
|
|
585
|
+
const staleNames = ["claude-usage-cache.json", ".claude-refresh-lock", "codex-rate-limits-cache.json"];
|
|
586
|
+
let cleaned = 0;
|
|
587
|
+
for (const name of staleNames) {
|
|
588
|
+
const fp = join(fCacheDir, name);
|
|
589
|
+
if (!existsSync(fp)) continue;
|
|
590
|
+
try {
|
|
591
|
+
const parsed = JSON.parse(readFileSync(fp, "utf8"));
|
|
592
|
+
if (parsed.error || name.startsWith(".")) { unlinkSync(fp); cleaned++; ok(`에러 캐시 정리: ${name}`); }
|
|
593
|
+
} catch { try { unlinkSync(fp); cleaned++; ok(`손상된 캐시 정리: ${name}`); } catch {} }
|
|
594
|
+
}
|
|
595
|
+
if (cleaned === 0) info("에러 캐시 없음");
|
|
596
|
+
console.log(`\n ${LINE}`);
|
|
597
|
+
info("수정 완료 — 아래 진단 결과를 확인하세요");
|
|
598
|
+
console.log("");
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
let issues = 0;
|
|
602
|
+
|
|
603
|
+
// 1. tfx-route.sh
|
|
604
|
+
section("tfx-route.sh");
|
|
605
|
+
const routeSh = join(CLAUDE_DIR, "scripts", "tfx-route.sh");
|
|
606
|
+
if (existsSync(routeSh)) {
|
|
607
|
+
const ver = getVersion(routeSh);
|
|
608
|
+
ok(`설치됨 ${ver ? `${DIM}v${ver}${RESET}` : ""}`);
|
|
609
|
+
} else {
|
|
610
|
+
fail("미설치 — tfx setup 실행 필요");
|
|
611
|
+
issues++;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// 2. HUD
|
|
615
|
+
section("HUD");
|
|
616
|
+
const hud = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
617
|
+
if (existsSync(hud)) {
|
|
618
|
+
ok("설치됨");
|
|
619
|
+
} else {
|
|
620
|
+
warn("미설치 ${GRAY}(선택사항)${RESET}");
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// 3. Codex CLI
|
|
624
|
+
section(`Codex CLI ${WHITE_BRIGHT}●${RESET}`);
|
|
625
|
+
issues += checkCliCrossShell("codex", "npm install -g @openai/codex");
|
|
626
|
+
if (which("codex")) {
|
|
627
|
+
if (process.env.OPENAI_API_KEY) {
|
|
628
|
+
ok("OPENAI_API_KEY 설정됨");
|
|
629
|
+
} else {
|
|
630
|
+
warn(`OPENAI_API_KEY 미설정 ${GRAY}(Pro 구독이면 불필요)${RESET}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// 4. Codex Profiles
|
|
635
|
+
section("Codex Profiles");
|
|
636
|
+
if (existsSync(CODEX_CONFIG_PATH)) {
|
|
637
|
+
const codexConfig = readFileSync(CODEX_CONFIG_PATH, "utf8");
|
|
638
|
+
for (const profile of REQUIRED_CODEX_PROFILES) {
|
|
639
|
+
if (hasProfileSection(codexConfig, profile.name)) {
|
|
640
|
+
ok(`${profile.name}: 정상`);
|
|
641
|
+
} else {
|
|
642
|
+
warn(`${profile.name}: 미설정`);
|
|
643
|
+
issues++;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
} else {
|
|
647
|
+
warn("config.toml 미존재");
|
|
648
|
+
issues++;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// 5. Gemini CLI
|
|
652
|
+
section(`Gemini CLI ${BLUE}●${RESET}`);
|
|
653
|
+
issues += checkCliCrossShell("gemini", "npm install -g @google/gemini-cli");
|
|
654
|
+
if (which("gemini")) {
|
|
655
|
+
if (process.env.GEMINI_API_KEY) {
|
|
656
|
+
ok("GEMINI_API_KEY 설정됨");
|
|
657
|
+
} else {
|
|
658
|
+
warn(`GEMINI_API_KEY 미설정 ${GRAY}(gemini auth login)${RESET}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// 6. Claude Code
|
|
663
|
+
section(`Claude Code ${AMBER}●${RESET}`);
|
|
664
|
+
const claudePath = which("claude");
|
|
665
|
+
if (claudePath) {
|
|
666
|
+
ok("설치됨");
|
|
667
|
+
} else {
|
|
668
|
+
fail("미설치 (필수)");
|
|
669
|
+
issues++;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// 7. 스킬 설치 상태
|
|
673
|
+
section("Skills");
|
|
674
|
+
const skillsSrc = join(PKG_ROOT, "skills");
|
|
675
|
+
const skillsDst = join(CLAUDE_DIR, "skills");
|
|
676
|
+
if (existsSync(skillsSrc)) {
|
|
677
|
+
let installed = 0;
|
|
678
|
+
let total = 0;
|
|
679
|
+
const missing = [];
|
|
680
|
+
for (const name of readdirSync(skillsSrc)) {
|
|
681
|
+
if (!existsSync(join(skillsSrc, name, "SKILL.md"))) continue;
|
|
682
|
+
total++;
|
|
683
|
+
if (existsSync(join(skillsDst, name, "SKILL.md"))) {
|
|
684
|
+
installed++;
|
|
685
|
+
} else {
|
|
686
|
+
missing.push(name);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (installed === total) {
|
|
690
|
+
ok(`${installed}/${total}개 설치됨`);
|
|
691
|
+
} else {
|
|
692
|
+
warn(`${installed}/${total}개 설치됨 — 미설치: ${missing.join(", ")}`);
|
|
693
|
+
info("triflux setup으로 동기화 가능");
|
|
694
|
+
issues++;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// 8. 플러그인 등록
|
|
699
|
+
section("Plugin");
|
|
700
|
+
const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
|
|
701
|
+
if (existsSync(pluginsFile)) {
|
|
702
|
+
const content = readFileSync(pluginsFile, "utf8");
|
|
703
|
+
if (content.includes("triflux")) {
|
|
704
|
+
ok("triflux 플러그인 등록됨");
|
|
705
|
+
} else {
|
|
706
|
+
warn("triflux 플러그인 미등록 — npm 단독 사용 중");
|
|
707
|
+
info("플러그인 등록: /plugin marketplace add <repo-url>");
|
|
708
|
+
}
|
|
709
|
+
} else {
|
|
710
|
+
info("플러그인 시스템 감지 안 됨 — npm 단독 사용");
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// 9. MCP 인벤토리
|
|
714
|
+
section("MCP Inventory");
|
|
715
|
+
const mcpCache = join(CLAUDE_DIR, "cache", "mcp-inventory.json");
|
|
716
|
+
if (existsSync(mcpCache)) {
|
|
717
|
+
try {
|
|
718
|
+
const inv = JSON.parse(readFileSync(mcpCache, "utf8"));
|
|
719
|
+
ok(`캐시 존재 (${inv.timestamp})`);
|
|
720
|
+
if (inv.codex?.servers?.length) {
|
|
721
|
+
const names = inv.codex.servers.map(s => s.name).join(", ");
|
|
722
|
+
info(`Codex: ${inv.codex.servers.length}개 서버 (${names})`);
|
|
723
|
+
}
|
|
724
|
+
if (inv.gemini?.servers?.length) {
|
|
725
|
+
const names = inv.gemini.servers.map(s => s.name).join(", ");
|
|
726
|
+
info(`Gemini: ${inv.gemini.servers.length}개 서버 (${names})`);
|
|
727
|
+
}
|
|
728
|
+
} catch {
|
|
729
|
+
warn("캐시 파일 파싱 실패");
|
|
730
|
+
}
|
|
731
|
+
} else {
|
|
732
|
+
warn("캐시 없음 — 다음 세션 시작 시 자동 생성");
|
|
733
|
+
info(`수동: node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}`);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// 10. CLI 이슈 트래커
|
|
737
|
+
section("CLI Issues");
|
|
738
|
+
const issuesFile = join(CLAUDE_DIR, "cache", "cli-issues.jsonl");
|
|
739
|
+
if (existsSync(issuesFile)) {
|
|
740
|
+
try {
|
|
741
|
+
const lines = readFileSync(issuesFile, "utf8").trim().split("\n").filter(Boolean);
|
|
742
|
+
const entries = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
743
|
+
const unresolved = entries.filter(e => !e.resolved);
|
|
744
|
+
|
|
745
|
+
if (unresolved.length === 0) {
|
|
746
|
+
ok("미해결 이슈 없음");
|
|
747
|
+
} else {
|
|
748
|
+
// 패턴별 그룹핑
|
|
749
|
+
const groups = {};
|
|
750
|
+
for (const e of unresolved) {
|
|
751
|
+
const key = `${e.cli}:${e.pattern}`;
|
|
752
|
+
if (!groups[key]) groups[key] = { ...e, count: 0 };
|
|
753
|
+
groups[key].count++;
|
|
754
|
+
if (e.ts > groups[key].ts) { groups[key].ts = e.ts; groups[key].snippet = e.snippet; }
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// 알려진 해결 버전 (패턴별 수정된 triflux 버전)
|
|
758
|
+
const KNOWN_FIXES = {
|
|
759
|
+
"gemini:deprecated_flag": "1.8.9", // -p → --prompt
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
const currentVer = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8")).version;
|
|
763
|
+
let cleaned = 0;
|
|
764
|
+
|
|
765
|
+
for (const [key, g] of Object.entries(groups)) {
|
|
766
|
+
const fixVer = KNOWN_FIXES[key];
|
|
767
|
+
if (fixVer && currentVer >= fixVer) {
|
|
768
|
+
// 해결된 이슈 — 자동 정리
|
|
769
|
+
cleaned += g.count;
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const age = Date.now() - g.ts;
|
|
773
|
+
const ago = age < 3600000 ? `${Math.round(age / 60000)}분 전` :
|
|
774
|
+
age < 86400000 ? `${Math.round(age / 3600000)}시간 전` :
|
|
775
|
+
`${Math.round(age / 86400000)}일 전`;
|
|
776
|
+
const sev = g.severity === "error" ? `${RED}ERROR${RESET}` : `${YELLOW}WARN${RESET}`;
|
|
777
|
+
warn(`[${sev}] ${g.cli}/${g.pattern} x${g.count} (최근: ${ago})`);
|
|
778
|
+
if (g.snippet) info(` ${g.snippet.substring(0, 120)}`);
|
|
779
|
+
if (fixVer) info(` 해결: triflux >= v${fixVer} (npm update -g triflux)`);
|
|
780
|
+
issues++;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// 해결된 이슈 자동 정리
|
|
784
|
+
if (cleaned > 0) {
|
|
785
|
+
const remaining = entries.filter(e => {
|
|
786
|
+
const key = `${e.cli}:${e.pattern}`;
|
|
787
|
+
const fixVer = KNOWN_FIXES[key];
|
|
788
|
+
return !(fixVer && currentVer >= fixVer);
|
|
789
|
+
});
|
|
790
|
+
writeFileSync(issuesFile, remaining.map(e => JSON.stringify(e)).join("\n") + (remaining.length ? "\n" : ""));
|
|
791
|
+
ok(`${cleaned}개 해결된 이슈 자동 정리됨`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
} catch (e) {
|
|
795
|
+
warn(`이슈 파일 읽기 실패: ${e.message}`);
|
|
796
|
+
}
|
|
797
|
+
} else {
|
|
798
|
+
ok("이슈 로그 없음 (정상)");
|
|
799
|
+
}
|
|
800
|
+
|
|
802
801
|
// 11. Team Sessions
|
|
803
802
|
section("Team Sessions");
|
|
804
803
|
const teamSessionReport = inspectTeamSessions();
|
|
@@ -874,741 +873,872 @@ async function cmdDoctor(options = {}) {
|
|
|
874
873
|
}
|
|
875
874
|
}
|
|
876
875
|
|
|
877
|
-
// 13.
|
|
878
|
-
section("
|
|
876
|
+
// 13. Stale Teams (Claude teams/ + tasks/ 자동 감지)
|
|
877
|
+
section("Stale Teams");
|
|
879
878
|
const teamsDir = join(CLAUDE_DIR, "teams");
|
|
880
879
|
const tasksDir = join(CLAUDE_DIR, "tasks");
|
|
881
|
-
if (existsSync(teamsDir)) {
|
|
882
|
-
try {
|
|
883
|
-
const teamDirs = readdirSync(teamsDir).filter(d => {
|
|
884
|
-
try { return statSync(join(teamsDir, d)).isDirectory(); } catch { return false; }
|
|
885
|
-
});
|
|
886
|
-
if (teamDirs.length === 0) {
|
|
887
|
-
ok("잔존 팀 없음");
|
|
888
|
-
} else {
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
if (existsSync(
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
return null;
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
try {
|
|
1366
|
-
const
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
if (
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
if (
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
880
|
+
if (existsSync(teamsDir)) {
|
|
881
|
+
try {
|
|
882
|
+
const teamDirs = readdirSync(teamsDir).filter(d => {
|
|
883
|
+
try { return statSync(join(teamsDir, d)).isDirectory(); } catch { return false; }
|
|
884
|
+
});
|
|
885
|
+
if (teamDirs.length === 0) {
|
|
886
|
+
ok("잔존 팀 없음");
|
|
887
|
+
} else {
|
|
888
|
+
const nowMs = Date.now();
|
|
889
|
+
const staleMaxAgeMs = STALE_TEAM_MAX_AGE_SEC * 1000;
|
|
890
|
+
const staleTeams = [];
|
|
891
|
+
const activeTeams = [];
|
|
892
|
+
|
|
893
|
+
for (const d of teamDirs) {
|
|
894
|
+
const teamPath = join(teamsDir, d);
|
|
895
|
+
const configPath = join(teamPath, "config.json");
|
|
896
|
+
let teamConfig = null;
|
|
897
|
+
let configMtimeMs = null;
|
|
898
|
+
|
|
899
|
+
// config.json 읽기 — createdAt 또는 mtime으로 나이 판정
|
|
900
|
+
try {
|
|
901
|
+
const configStat = statSync(configPath);
|
|
902
|
+
configMtimeMs = configStat.mtimeMs;
|
|
903
|
+
teamConfig = JSON.parse(readFileSync(configPath, "utf8"));
|
|
904
|
+
} catch {
|
|
905
|
+
// config.json 없으면 디렉토리 mtime 사용
|
|
906
|
+
try { configMtimeMs = statSync(teamPath).mtimeMs; } catch {}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const createdAtMs = teamConfig?.createdAt ?? configMtimeMs;
|
|
910
|
+
const ageMs = createdAtMs != null ? Math.max(0, nowMs - createdAtMs) : null;
|
|
911
|
+
const ageSec = ageMs != null ? Math.floor(ageMs / 1000) : null;
|
|
912
|
+
const aged = ageMs != null && ageMs >= staleMaxAgeMs;
|
|
913
|
+
|
|
914
|
+
// 활성 멤버 확인 — leadSessionId 또는 멤버 agentId로 프로세스 검색
|
|
915
|
+
let hasActiveMember = false;
|
|
916
|
+
if (teamConfig?.members?.length > 0) {
|
|
917
|
+
const searchTokens = [];
|
|
918
|
+
if (teamConfig.leadSessionId) searchTokens.push(teamConfig.leadSessionId.toLowerCase());
|
|
919
|
+
if (teamConfig.name) searchTokens.push(teamConfig.name.toLowerCase());
|
|
920
|
+
for (const member of teamConfig.members) {
|
|
921
|
+
if (member.agentId) searchTokens.push(member.agentId.split("@")[0].toLowerCase());
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// tmux 세션 이름과 매칭
|
|
925
|
+
const liveSessionNames = teamSessionReport.sessions.map(s => s.sessionName.toLowerCase());
|
|
926
|
+
hasActiveMember = searchTokens.some(token =>
|
|
927
|
+
liveSessionNames.some(name => name.includes(token))
|
|
928
|
+
);
|
|
929
|
+
|
|
930
|
+
// 프로세스 명령줄에서 세션 ID 매칭 (tmux 없는 in-process 팀 지원)
|
|
931
|
+
if (!hasActiveMember && teamConfig.leadSessionId) {
|
|
932
|
+
try {
|
|
933
|
+
const sessionToken = teamConfig.leadSessionId.toLowerCase();
|
|
934
|
+
// Claude Code 프로세스에서 세션 ID 검색
|
|
935
|
+
if (process.platform === "win32") {
|
|
936
|
+
const psOut = execSync(
|
|
937
|
+
`powershell -NoProfile -Command "$ErrorActionPreference='SilentlyContinue'; Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match '${teamConfig.leadSessionId.slice(0, 8)}' } | Select-Object ProcessId | ConvertTo-Json -Compress"`,
|
|
938
|
+
{ encoding: "utf8", timeout: 8000, stdio: ["ignore", "pipe", "ignore"], windowsHide: true },
|
|
939
|
+
).trim();
|
|
940
|
+
if (psOut && psOut !== "null") {
|
|
941
|
+
const parsed = JSON.parse(psOut);
|
|
942
|
+
const procs = Array.isArray(parsed) ? parsed : [parsed];
|
|
943
|
+
hasActiveMember = procs.some(p => p.ProcessId > 0);
|
|
944
|
+
}
|
|
945
|
+
} else {
|
|
946
|
+
const psOut = execSync(
|
|
947
|
+
`ps -ax -o pid=,command= | grep -i '${teamConfig.leadSessionId.slice(0, 8)}' | grep -v grep`,
|
|
948
|
+
{ encoding: "utf8", timeout: 5000, stdio: ["ignore", "pipe", "ignore"] },
|
|
949
|
+
).trim();
|
|
950
|
+
hasActiveMember = psOut.length > 0;
|
|
951
|
+
}
|
|
952
|
+
} catch {
|
|
953
|
+
// 프로세스 검색 실패 — stale로 간주하지 않음 (보수적)
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const stale = aged && !hasActiveMember;
|
|
959
|
+
const teamEntry = {
|
|
960
|
+
name: d,
|
|
961
|
+
teamName: teamConfig?.name || d,
|
|
962
|
+
description: teamConfig?.description || null,
|
|
963
|
+
memberCount: teamConfig?.members?.length || 0,
|
|
964
|
+
ageSec,
|
|
965
|
+
stale,
|
|
966
|
+
hasActiveMember,
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
if (stale) {
|
|
970
|
+
staleTeams.push(teamEntry);
|
|
971
|
+
} else {
|
|
972
|
+
activeTeams.push(teamEntry);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// 활성 팀 표시
|
|
977
|
+
for (const t of activeTeams) {
|
|
978
|
+
const ageLabel = formatElapsedAge(t.ageSec);
|
|
979
|
+
const memberLabel = `${t.memberCount}명`;
|
|
980
|
+
ok(`${t.name}: 활성 (경과=${ageLabel}, 멤버=${memberLabel})`);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// stale 팀 표시 및 정리
|
|
984
|
+
if (staleTeams.length === 0 && activeTeams.length > 0) {
|
|
985
|
+
ok("stale 팀 없음");
|
|
986
|
+
} else if (staleTeams.length > 0) {
|
|
987
|
+
warn(`${staleTeams.length}개 stale 팀 발견`);
|
|
988
|
+
for (const t of staleTeams) {
|
|
989
|
+
const ageLabel = formatElapsedAge(t.ageSec);
|
|
990
|
+
warn(`${t.name}: stale (경과=${ageLabel}, 멤버=${t.memberCount}명, 활성 프로세스 없음)`);
|
|
991
|
+
if (t.description) info(`설명: ${t.description}`);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (fix) {
|
|
995
|
+
let cleaned = 0;
|
|
996
|
+
for (const t of staleTeams) {
|
|
997
|
+
try {
|
|
998
|
+
rmSync(join(teamsDir, t.name), { recursive: true, force: true });
|
|
999
|
+
cleaned++;
|
|
1000
|
+
ok(`stale 팀 정리: ${t.name}`);
|
|
1001
|
+
} catch (e) {
|
|
1002
|
+
fail(`팀 정리 실패: ${t.name} — ${e.message}`);
|
|
1003
|
+
}
|
|
1004
|
+
// 연관 tasks 디렉토리도 정리
|
|
1005
|
+
const taskDir = join(tasksDir, t.name);
|
|
1006
|
+
if (existsSync(taskDir)) {
|
|
1007
|
+
try {
|
|
1008
|
+
rmSync(taskDir, { recursive: true, force: true });
|
|
1009
|
+
ok(`연관 tasks 정리: ${t.name}`);
|
|
1010
|
+
} catch {}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
info(`${cleaned}/${staleTeams.length}개 stale 팀 정리 완료`);
|
|
1014
|
+
} else {
|
|
1015
|
+
info("정리: tfx doctor --fix");
|
|
1016
|
+
issues += staleTeams.length;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
} catch (e) {
|
|
1021
|
+
warn(`teams 디렉토리 읽기 실패: ${e.message}`);
|
|
1022
|
+
}
|
|
1023
|
+
} else {
|
|
1024
|
+
ok("잔존 팀 없음");
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// 결과
|
|
1028
|
+
console.log(`\n ${LINE}`);
|
|
1029
|
+
if (issues === 0) {
|
|
1030
|
+
console.log(` ${GREEN_BRIGHT}${BOLD}✓ 모든 검사 통과${RESET}\n`);
|
|
1031
|
+
} else {
|
|
1032
|
+
console.log(` ${YELLOW}${BOLD}⚠ ${issues}개 항목 확인 필요${RESET}\n`);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function cmdUpdate() {
|
|
1037
|
+
const isDev = isDevUpdateRequested(process.argv);
|
|
1038
|
+
const tagLabel = isDev ? ` ${YELLOW}--dev${RESET}` : "";
|
|
1039
|
+
console.log(`\n${BOLD}triflux update${RESET}${tagLabel}\n`);
|
|
1040
|
+
|
|
1041
|
+
// 1. 설치 방식 감지
|
|
1042
|
+
const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
|
|
1043
|
+
let installMode = "unknown";
|
|
1044
|
+
let pluginPath = null;
|
|
1045
|
+
|
|
1046
|
+
// 플러그인 모드 감지
|
|
1047
|
+
if (existsSync(pluginsFile)) {
|
|
1048
|
+
try {
|
|
1049
|
+
const plugins = JSON.parse(readFileSync(pluginsFile, "utf8"));
|
|
1050
|
+
for (const [key, entries] of Object.entries(plugins.plugins || {})) {
|
|
1051
|
+
if (key.startsWith("triflux")) {
|
|
1052
|
+
pluginPath = entries[0]?.installPath;
|
|
1053
|
+
installMode = "plugin";
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
} catch {}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// PKG_ROOT가 플러그인 캐시 내에 있으면 플러그인 모드
|
|
1061
|
+
if (installMode === "unknown" && PKG_ROOT.includes(join(".claude", "plugins"))) {
|
|
1062
|
+
installMode = "plugin";
|
|
1063
|
+
pluginPath = PKG_ROOT;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// npm global 감지
|
|
1067
|
+
if (installMode === "unknown") {
|
|
1068
|
+
try {
|
|
1069
|
+
const npmList = execSync("npm list -g triflux --depth=0", {
|
|
1070
|
+
encoding: "utf8",
|
|
1071
|
+
timeout: 10000,
|
|
1072
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1073
|
+
});
|
|
1074
|
+
if (npmList.includes("triflux")) installMode = "npm-global";
|
|
1075
|
+
} catch {}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// npm local 감지
|
|
1079
|
+
if (installMode === "unknown") {
|
|
1080
|
+
const localPkg = join(process.cwd(), "node_modules", "triflux");
|
|
1081
|
+
if (existsSync(localPkg)) installMode = "npm-local";
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// git 저장소 직접 사용
|
|
1085
|
+
if (installMode === "unknown" && existsSync(join(PKG_ROOT, ".git"))) {
|
|
1086
|
+
installMode = "git-local";
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
info(`검색: ${installMode === "plugin" ? "플러그인" : installMode === "npm-global" ? "npm global" : installMode === "npm-local" ? "npm local" : installMode === "git-local" ? "git 로컬 저장소" : "알 수 없음"} 설치 감지`);
|
|
1090
|
+
|
|
1091
|
+
// 2. 설치 방식에 따라 업데이트
|
|
1092
|
+
const oldVer = PKG.version;
|
|
1093
|
+
let updated = false;
|
|
1094
|
+
let stoppedHubInfo = null;
|
|
1095
|
+
|
|
1096
|
+
try {
|
|
1097
|
+
switch (installMode) {
|
|
1098
|
+
case "plugin": {
|
|
1099
|
+
const gitDir = pluginPath || PKG_ROOT;
|
|
1100
|
+
const result = execSync("git pull", {
|
|
1101
|
+
encoding: "utf8",
|
|
1102
|
+
timeout: 30000,
|
|
1103
|
+
cwd: gitDir,
|
|
1104
|
+
}).trim();
|
|
1105
|
+
ok(`git pull — ${result}`);
|
|
1106
|
+
updated = true;
|
|
1107
|
+
break;
|
|
1108
|
+
}
|
|
1109
|
+
case "npm-global": {
|
|
1110
|
+
stoppedHubInfo = stopHubForUpdate();
|
|
1111
|
+
if (stoppedHubInfo?.pid) {
|
|
1112
|
+
info(`실행 중 hub 정지 (PID ${stoppedHubInfo.pid})`);
|
|
1113
|
+
}
|
|
1114
|
+
const npmCmd = isDev ? "npm install -g triflux@dev" : "npm update -g triflux";
|
|
1115
|
+
const result = execSync(npmCmd, {
|
|
1116
|
+
encoding: "utf8",
|
|
1117
|
+
timeout: 60000,
|
|
1118
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1119
|
+
}).trim().split(/\r?\n/)[0];
|
|
1120
|
+
ok(`${isDev ? "npm install -g triflux@dev" : "npm update -g triflux"} — ${result || "완료"}`);
|
|
1121
|
+
updated = true;
|
|
1122
|
+
break;
|
|
1123
|
+
}
|
|
1124
|
+
case "npm-local": {
|
|
1125
|
+
const npmLocalCmd = isDev ? "npm install triflux@dev" : "npm update triflux";
|
|
1126
|
+
const result = execSync(npmLocalCmd, {
|
|
1127
|
+
encoding: "utf8",
|
|
1128
|
+
timeout: 60000,
|
|
1129
|
+
cwd: process.cwd(),
|
|
1130
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1131
|
+
}).trim().split(/\r?\n/)[0];
|
|
1132
|
+
ok(`${isDev ? "npm install triflux@dev" : "npm update triflux"} — ${result || "완료"}`);
|
|
1133
|
+
updated = true;
|
|
1134
|
+
break;
|
|
1135
|
+
}
|
|
1136
|
+
case "git-local": {
|
|
1137
|
+
const result = execSync("git pull", {
|
|
1138
|
+
encoding: "utf8",
|
|
1139
|
+
timeout: 30000,
|
|
1140
|
+
cwd: PKG_ROOT,
|
|
1141
|
+
}).trim();
|
|
1142
|
+
ok(`git pull — ${result}`);
|
|
1143
|
+
updated = true;
|
|
1144
|
+
break;
|
|
1145
|
+
}
|
|
1146
|
+
default:
|
|
1147
|
+
fail("설치 방식을 감지할 수 없음");
|
|
1148
|
+
info("수동 업데이트: cd <triflux-dir> && git pull");
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
} catch (e) {
|
|
1152
|
+
if (stoppedHubInfo && startHubAfterUpdate(stoppedHubInfo)) {
|
|
1153
|
+
info("업데이트 실패 후 hub 재기동 시도");
|
|
1154
|
+
}
|
|
1155
|
+
fail(`업데이트 실패: ${e.message}`);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// 3. setup 재실행 (tfx-route.sh, HUD, 스킬 동기화)
|
|
1160
|
+
if (updated) {
|
|
1161
|
+
console.log("");
|
|
1162
|
+
// 업데이트 후 새 버전 읽기
|
|
1163
|
+
let newVer = oldVer;
|
|
1164
|
+
try {
|
|
1165
|
+
const newPkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
1166
|
+
newVer = newPkg.version;
|
|
1167
|
+
} catch {}
|
|
1168
|
+
|
|
1169
|
+
if (newVer !== oldVer) {
|
|
1170
|
+
ok(`버전: v${oldVer} → v${newVer}`);
|
|
1171
|
+
} else {
|
|
1172
|
+
ok(`버전: v${oldVer} (이미 최신)`);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// setup 재실행
|
|
1176
|
+
console.log("");
|
|
1177
|
+
info("setup 재실행 중...");
|
|
1178
|
+
cmdSetup();
|
|
1179
|
+
|
|
1180
|
+
if (stoppedHubInfo) {
|
|
1181
|
+
if (startHubAfterUpdate(stoppedHubInfo)) info("hub 재기동 완료");
|
|
1182
|
+
else warn("hub 재기동 실패 — `tfx hub start`로 수동 시작 필요");
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
console.log(`${GREEN}${BOLD}업데이트 완료${RESET}\n`);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
function cmdList() {
|
|
1190
|
+
console.log(`\n ${AMBER}${BOLD}⬡ triflux list${RESET} ${VER}\n`);
|
|
1191
|
+
console.log(` ${LINE}`);
|
|
1192
|
+
|
|
1193
|
+
const pluginSkills = join(PKG_ROOT, "skills");
|
|
1194
|
+
const installedSkills = join(CLAUDE_DIR, "skills");
|
|
1195
|
+
|
|
1196
|
+
section("패키지 스킬");
|
|
1197
|
+
if (existsSync(pluginSkills)) {
|
|
1198
|
+
for (const name of readdirSync(pluginSkills).sort()) {
|
|
1199
|
+
const src = join(pluginSkills, name, "SKILL.md");
|
|
1200
|
+
if (!existsSync(src)) continue;
|
|
1201
|
+
const dst = join(installedSkills, name, "SKILL.md");
|
|
1202
|
+
const installed = existsSync(dst);
|
|
1203
|
+
if (installed) {
|
|
1204
|
+
console.log(` ${GREEN_BRIGHT}✓${RESET} ${BOLD}${name}${RESET}`);
|
|
1205
|
+
} else {
|
|
1206
|
+
console.log(` ${RED_BRIGHT}✗${RESET} ${DIM}${name}${RESET} ${GRAY}(미설치)${RESET}`);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
section("사용자 스킬");
|
|
1212
|
+
const pkgNames = new Set(existsSync(pluginSkills) ? readdirSync(pluginSkills) : []);
|
|
1213
|
+
let userCount = 0;
|
|
1214
|
+
if (existsSync(installedSkills)) {
|
|
1215
|
+
for (const name of readdirSync(installedSkills).sort()) {
|
|
1216
|
+
if (pkgNames.has(name)) continue;
|
|
1217
|
+
const skill = join(installedSkills, name, "SKILL.md");
|
|
1218
|
+
if (!existsSync(skill)) continue;
|
|
1219
|
+
console.log(` ${AMBER}◆${RESET} ${name}`);
|
|
1220
|
+
userCount++;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
if (userCount === 0) console.log(` ${GRAY}없음${RESET}`);
|
|
1224
|
+
|
|
1225
|
+
console.log(`\n ${LINE}`);
|
|
1226
|
+
console.log(` ${GRAY}${installedSkills}${RESET}\n`);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function cmdVersion() {
|
|
1230
|
+
const routeVer = getVersion(join(CLAUDE_DIR, "scripts", "tfx-route.sh"));
|
|
1231
|
+
const hudVer = getVersion(join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"));
|
|
1232
|
+
console.log(`\n ${AMBER}${BOLD}⬡ triflux${RESET} ${WHITE_BRIGHT}v${PKG.version}${RESET}`);
|
|
1233
|
+
if (routeVer) console.log(` ${GRAY}tfx-route${RESET} v${routeVer}`);
|
|
1234
|
+
if (hudVer) console.log(` ${GRAY}hud${RESET} v${hudVer}`);
|
|
1235
|
+
console.log("");
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function checkForUpdate() {
|
|
1239
|
+
const cacheFile = join(CLAUDE_DIR, "cache", "triflux-update-check.json");
|
|
1240
|
+
const cacheDir = dirname(cacheFile);
|
|
1241
|
+
|
|
1242
|
+
// 캐시 확인 (1시간 이내면 캐시 사용)
|
|
1243
|
+
try {
|
|
1244
|
+
if (existsSync(cacheFile)) {
|
|
1245
|
+
const cache = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
1246
|
+
if (Date.now() - cache.timestamp < 3600000) {
|
|
1247
|
+
return cache.latest !== PKG.version ? cache.latest : null;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
} catch {}
|
|
1251
|
+
|
|
1252
|
+
// npm registry 조회
|
|
1253
|
+
try {
|
|
1254
|
+
const result = execSync("npm view triflux version", {
|
|
1255
|
+
encoding: "utf8",
|
|
1256
|
+
timeout: 5000,
|
|
1257
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1258
|
+
}).trim();
|
|
1259
|
+
|
|
1260
|
+
if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true });
|
|
1261
|
+
writeFileSync(cacheFile, JSON.stringify({ latest: result, timestamp: Date.now() }));
|
|
1262
|
+
|
|
1263
|
+
return result !== PKG.version ? result : null;
|
|
1264
|
+
} catch {
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
function cmdHelp() {
|
|
1270
|
+
const latestVer = checkForUpdate();
|
|
1271
|
+
const updateNotice = latestVer
|
|
1272
|
+
? `\n ${YELLOW}${BOLD}↑ v${latestVer} 사용 가능${RESET} ${GRAY}npm update -g triflux${RESET}\n`
|
|
1273
|
+
: "";
|
|
1274
|
+
|
|
1275
|
+
console.log(`
|
|
1276
|
+
${AMBER}${BOLD}⬡ triflux${RESET} ${DIM}v${PKG.version}${RESET}
|
|
1277
|
+
${GRAY}CLI-first multi-model orchestrator for Claude Code${RESET}
|
|
1278
|
+
${updateNotice}
|
|
1279
|
+
${LINE}
|
|
1280
|
+
|
|
1281
|
+
${BOLD}Commands${RESET}
|
|
1282
|
+
|
|
1283
|
+
${WHITE_BRIGHT}tfx setup${RESET} ${GRAY}파일 동기화 + HUD 설정${RESET}
|
|
1284
|
+
${WHITE_BRIGHT}tfx doctor${RESET} ${GRAY}CLI 진단 + 이슈 확인${RESET}
|
|
1285
|
+
${DIM} --fix${RESET} ${GRAY}진단 + 자동 수정${RESET}
|
|
1286
|
+
${DIM} --reset${RESET} ${GRAY}캐시 전체 초기화${RESET}
|
|
1287
|
+
${WHITE_BRIGHT}tfx update${RESET} ${GRAY}최신 안정 버전으로 업데이트${RESET}
|
|
1288
|
+
${DIM} --dev / dev${RESET} ${GRAY}dev 태그로 업데이트${RESET}
|
|
1289
|
+
${WHITE_BRIGHT}tfx list${RESET} ${GRAY}설치된 스킬 목록${RESET}
|
|
1290
|
+
${WHITE_BRIGHT}tfx hub${RESET} ${GRAY}MCP 메시지 버스 관리 (start/stop/status)${RESET}
|
|
1291
|
+
${WHITE_BRIGHT}tfx multi${RESET} ${GRAY}멀티-CLI 팀 모드 (tmux + Hub)${RESET}
|
|
1292
|
+
${WHITE_BRIGHT}tfx codex-team${RESET} ${GRAY}Codex 전용 팀 모드 (기본 lead/agents: codex)${RESET}
|
|
1293
|
+
${WHITE_BRIGHT}tfx notion-read${RESET} ${GRAY}Notion 페이지 → 마크다운 (Codex/Gemini MCP)${RESET}
|
|
1294
|
+
${WHITE_BRIGHT}tfx version${RESET} ${GRAY}버전 표시${RESET}
|
|
1295
|
+
|
|
1296
|
+
${BOLD}Skills${RESET} ${GRAY}(Claude Code 슬래시 커맨드)${RESET}
|
|
1297
|
+
|
|
1298
|
+
${AMBER}/tfx-auto${RESET} ${GRAY}자동 분류 + 병렬 실행${RESET}
|
|
1299
|
+
${WHITE_BRIGHT}/tfx-auto-codex${RESET} ${GRAY}Codex 리드 + Gemini 유지 (no-Claude-native)${RESET}
|
|
1300
|
+
${WHITE_BRIGHT}/tfx-codex${RESET} ${GRAY}Codex 전용 모드${RESET}
|
|
1301
|
+
${BLUE}/tfx-gemini${RESET} ${GRAY}Gemini 전용 모드${RESET}
|
|
1302
|
+
${AMBER}/tfx-setup${RESET} ${GRAY}HUD 설정 + 진단${RESET}
|
|
1303
|
+
${YELLOW}/tfx-doctor${RESET} ${GRAY}진단 + 수리 + 캐시 초기화${RESET}
|
|
1304
|
+
|
|
1305
|
+
${LINE}
|
|
1306
|
+
${GRAY}github.com/tellang/triflux${RESET}
|
|
1307
|
+
`);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
async function cmdCodexTeam() {
|
|
1311
|
+
const args = process.argv.slice(3);
|
|
1312
|
+
const sub = String(args[0] || "").toLowerCase();
|
|
1313
|
+
const passthrough = new Set([
|
|
1314
|
+
"status", "attach", "stop", "kill", "send", "list", "help", "--help", "-h",
|
|
1315
|
+
"tasks", "task", "focus", "interrupt", "control", "debug",
|
|
1316
|
+
]);
|
|
1317
|
+
|
|
1318
|
+
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
1319
|
+
console.log(`
|
|
1320
|
+
${AMBER}${BOLD}⬡ tfx codex-team${RESET}
|
|
1321
|
+
|
|
1322
|
+
${WHITE_BRIGHT}tfx codex-team "작업"${RESET} ${GRAY}Codex 리드 + 워커 2개로 팀 시작${RESET}
|
|
1323
|
+
${WHITE_BRIGHT}tfx codex-team --layout 1xN "작업"${RESET} ${GRAY}(세로 분할 컬럼)${RESET}
|
|
1324
|
+
${WHITE_BRIGHT}tfx codex-team --layout Nx1 "작업"${RESET} ${GRAY}(가로 분할 스택)${RESET}
|
|
1325
|
+
${WHITE_BRIGHT}tfx codex-team status${RESET}
|
|
1326
|
+
${WHITE_BRIGHT}tfx codex-team debug --lines 30${RESET}
|
|
1327
|
+
${WHITE_BRIGHT}tfx codex-team send N "msg"${RESET}
|
|
1328
|
+
|
|
1329
|
+
${DIM}내부적으로 tfx multi을 호출하며, 시작 시 --lead codex --agents codex,codex를 기본 주입합니다.${RESET}
|
|
1330
|
+
`);
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const hasAgents = args.includes("--agents");
|
|
1335
|
+
const hasLead = args.includes("--lead");
|
|
1336
|
+
const hasLayout = args.includes("--layout");
|
|
1337
|
+
const isControl = passthrough.has(sub);
|
|
1338
|
+
const normalizedArgs = isControl && args.length ? [sub, ...args.slice(1)] : args;
|
|
1339
|
+
const inject = [];
|
|
1340
|
+
if (!isControl && !hasLead) inject.push("--lead", "codex");
|
|
1341
|
+
if (!isControl && !hasAgents) inject.push("--agents", "codex,codex");
|
|
1342
|
+
if (!isControl && !hasLayout) inject.push("--layout", "1xN");
|
|
1343
|
+
const forwarded = isControl ? normalizedArgs : [...inject, ...args];
|
|
1344
|
+
|
|
1345
|
+
const prevArgv = process.argv;
|
|
1346
|
+
const prevProfile = process.env.TFX_TEAM_PROFILE;
|
|
1347
|
+
process.env.TFX_TEAM_PROFILE = "codex-team";
|
|
1348
|
+
const { pathToFileURL } = await import("node:url");
|
|
1349
|
+
const { cmdTeam } = await import(pathToFileURL(join(PKG_ROOT, "hub", "team", "cli.mjs")).href);
|
|
1350
|
+
process.argv = [prevArgv[0], prevArgv[1], "team", ...forwarded];
|
|
1351
|
+
try {
|
|
1352
|
+
await cmdTeam();
|
|
1353
|
+
} finally {
|
|
1354
|
+
process.argv = prevArgv;
|
|
1355
|
+
if (typeof prevProfile === "string") process.env.TFX_TEAM_PROFILE = prevProfile;
|
|
1356
|
+
else delete process.env.TFX_TEAM_PROFILE;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// ── Hub preflight 체크 (multi/auto 실행 전) ──
|
|
1361
|
+
|
|
1362
|
+
async function checkHubRunning() {
|
|
1363
|
+
const port = Number(process.env.TFX_HUB_PORT || "27888");
|
|
1364
|
+
try {
|
|
1365
|
+
const res = await fetch(`http://127.0.0.1:${port}/status`, {
|
|
1366
|
+
signal: AbortSignal.timeout(2000),
|
|
1367
|
+
});
|
|
1368
|
+
if (res.ok) return true;
|
|
1369
|
+
} catch {}
|
|
1370
|
+
console.log("");
|
|
1371
|
+
warn(`${AMBER}tfx-hub${RESET}가 실행되고 있지 않습니다.`);
|
|
1372
|
+
info(`Hub 없이 실행하면 Claude 네이티브 에이전트로 폴백되어 토큰이 소비됩니다.`);
|
|
1373
|
+
info(`Codex(무료) 위임을 활용하려면 먼저 Hub를 시작하세요:\n`);
|
|
1374
|
+
console.log(` ${WHITE_BRIGHT}tfx hub start${RESET}\n`);
|
|
1375
|
+
return false;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// ── hub 서브커맨드 ──
|
|
1379
|
+
|
|
1380
|
+
const HUB_PID_DIR = join(homedir(), ".claude", "cache", "tfx-hub");
|
|
1381
|
+
const HUB_PID_FILE = join(HUB_PID_DIR, "hub.pid");
|
|
1382
|
+
|
|
1383
|
+
function sleepMs(ms) {
|
|
1384
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function stopHubForUpdate() {
|
|
1388
|
+
if (!existsSync(HUB_PID_FILE)) return null;
|
|
1389
|
+
let info = null;
|
|
1390
|
+
try {
|
|
1391
|
+
info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1392
|
+
process.kill(info.pid, 0);
|
|
1393
|
+
} catch {
|
|
1394
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1395
|
+
return null;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
try {
|
|
1399
|
+
if (process.platform === "win32") {
|
|
1400
|
+
execFileSync("taskkill", ["/PID", String(info.pid), "/T", "/F"], {
|
|
1401
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1402
|
+
timeout: 10000,
|
|
1403
|
+
});
|
|
1404
|
+
} else {
|
|
1405
|
+
process.kill(info.pid, "SIGTERM");
|
|
1406
|
+
}
|
|
1407
|
+
} catch {
|
|
1408
|
+
try { process.kill(info.pid, "SIGKILL"); } catch {}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
sleepMs(300);
|
|
1412
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1413
|
+
return info;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function startHubAfterUpdate(info) {
|
|
1417
|
+
if (!info) return false;
|
|
1418
|
+
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
1419
|
+
if (!existsSync(serverPath)) return false;
|
|
1420
|
+
const port = Number(info?.port) > 0 ? String(info.port) : String(process.env.TFX_HUB_PORT || "27888");
|
|
1421
|
+
|
|
1422
|
+
try {
|
|
1423
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
1424
|
+
env: { ...process.env, TFX_HUB_PORT: port },
|
|
1425
|
+
stdio: "ignore",
|
|
1426
|
+
detached: true,
|
|
1427
|
+
});
|
|
1428
|
+
child.unref();
|
|
1429
|
+
return true;
|
|
1430
|
+
} catch {
|
|
1431
|
+
return false;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// 설치된 CLI에 tfx-hub MCP 서버 자동 등록 (1회 설정, 이후 재실행 불필요)
|
|
1436
|
+
function autoRegisterMcp(mcpUrl) {
|
|
1437
|
+
section("MCP 자동 등록");
|
|
1438
|
+
|
|
1439
|
+
// Codex — codex mcp add
|
|
1440
|
+
if (which("codex")) {
|
|
1441
|
+
try {
|
|
1442
|
+
// 이미 등록됐는지 확인
|
|
1443
|
+
const list = execSync("codex mcp list 2>&1", { encoding: "utf8", timeout: 10000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1444
|
+
if (list.includes("tfx-hub")) {
|
|
1445
|
+
ok("Codex: 이미 등록됨");
|
|
1446
|
+
} else {
|
|
1447
|
+
execFileSync("codex", ["mcp", "add", "tfx-hub", "--url", mcpUrl], { timeout: 10000, stdio: "ignore" });
|
|
1448
|
+
ok("Codex: MCP 등록 완료");
|
|
1449
|
+
}
|
|
1450
|
+
} catch {
|
|
1451
|
+
// mcp list/add 미지원 → 설정 파일 직접 수정
|
|
1452
|
+
try {
|
|
1453
|
+
const codexDir = join(homedir(), ".codex");
|
|
1454
|
+
const configFile = join(codexDir, "config.json");
|
|
1455
|
+
let config = {};
|
|
1456
|
+
if (existsSync(configFile)) config = JSON.parse(readFileSync(configFile, "utf8"));
|
|
1457
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
1458
|
+
if (!config.mcpServers["tfx-hub"]) {
|
|
1459
|
+
config.mcpServers["tfx-hub"] = { url: mcpUrl };
|
|
1460
|
+
if (!existsSync(codexDir)) mkdirSync(codexDir, { recursive: true });
|
|
1461
|
+
writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n");
|
|
1462
|
+
ok("Codex: config.json에 등록 완료");
|
|
1463
|
+
} else {
|
|
1464
|
+
ok("Codex: 이미 등록됨");
|
|
1465
|
+
}
|
|
1466
|
+
} catch (e) { warn(`Codex 등록 실패: ${e.message}`); }
|
|
1467
|
+
}
|
|
1468
|
+
} else {
|
|
1469
|
+
info("Codex: 미설치 (건너뜀)");
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// Gemini — settings.json 직접 수정
|
|
1473
|
+
if (which("gemini")) {
|
|
1474
|
+
try {
|
|
1475
|
+
const geminiDir = join(homedir(), ".gemini");
|
|
1476
|
+
const settingsFile = join(geminiDir, "settings.json");
|
|
1477
|
+
let settings = {};
|
|
1478
|
+
if (existsSync(settingsFile)) settings = JSON.parse(readFileSync(settingsFile, "utf8"));
|
|
1479
|
+
if (!settings.mcpServers) settings.mcpServers = {};
|
|
1480
|
+
if (!settings.mcpServers["tfx-hub"]) {
|
|
1481
|
+
settings.mcpServers["tfx-hub"] = { url: mcpUrl };
|
|
1482
|
+
if (!existsSync(geminiDir)) mkdirSync(geminiDir, { recursive: true });
|
|
1483
|
+
writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
|
|
1484
|
+
ok("Gemini: settings.json에 등록 완료");
|
|
1485
|
+
} else {
|
|
1486
|
+
ok("Gemini: 이미 등록됨");
|
|
1487
|
+
}
|
|
1488
|
+
} catch (e) { warn(`Gemini 등록 실패: ${e.message}`); }
|
|
1489
|
+
} else {
|
|
1490
|
+
info("Gemini: 미설치 (건너뜀)");
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// Claude — 프로젝트 .mcp.json에 등록 (오케스트레이터용)
|
|
1494
|
+
try {
|
|
1495
|
+
const mcpJsonPath = join(PKG_ROOT, ".mcp.json");
|
|
1496
|
+
let mcpJson = {};
|
|
1497
|
+
if (existsSync(mcpJsonPath)) mcpJson = JSON.parse(readFileSync(mcpJsonPath, "utf8"));
|
|
1498
|
+
if (!mcpJson.mcpServers) mcpJson.mcpServers = {};
|
|
1499
|
+
if (!mcpJson.mcpServers["tfx-hub"]) {
|
|
1500
|
+
mcpJson.mcpServers["tfx-hub"] = { type: "url", url: mcpUrl };
|
|
1501
|
+
writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + "\n");
|
|
1502
|
+
ok("Claude: .mcp.json에 등록 완료");
|
|
1503
|
+
} else {
|
|
1504
|
+
ok("Claude: 이미 등록됨");
|
|
1505
|
+
}
|
|
1506
|
+
} catch (e) { warn(`Claude 등록 실패: ${e.message}`); }
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
async function cmdHub() {
|
|
1510
|
+
const sub = process.argv[3] || "status";
|
|
1511
|
+
const defaultPortRaw = Number(process.env.TFX_HUB_PORT || "27888");
|
|
1512
|
+
const probePort = Number.isFinite(defaultPortRaw) && defaultPortRaw > 0 ? defaultPortRaw : 27888;
|
|
1513
|
+
const formatHostForUrl = (host) => host.includes(":") ? `[${host}]` : host;
|
|
1514
|
+
const probeHubStatus = async (host = "127.0.0.1", port = probePort, timeoutMs = 3000) => {
|
|
1515
|
+
try {
|
|
1516
|
+
const res = await fetch(`http://${formatHostForUrl(host)}:${port}/status`, {
|
|
1517
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1518
|
+
});
|
|
1519
|
+
if (!res.ok) return null;
|
|
1520
|
+
const data = await res.json();
|
|
1521
|
+
return data?.hub ? data : null;
|
|
1522
|
+
} catch {
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
const recoverPidFile = (statusData, defaultHost = "127.0.0.1") => {
|
|
1527
|
+
const pid = Number(statusData?.pid);
|
|
1528
|
+
const port = Number(statusData?.port) || probePort;
|
|
1529
|
+
if (!Number.isFinite(pid) || pid <= 0) return;
|
|
1530
|
+
try {
|
|
1531
|
+
mkdirSync(HUB_PID_DIR, { recursive: true });
|
|
1532
|
+
writeFileSync(HUB_PID_FILE, JSON.stringify({
|
|
1533
|
+
pid,
|
|
1534
|
+
port,
|
|
1535
|
+
host: defaultHost,
|
|
1536
|
+
url: `http://${formatHostForUrl(defaultHost)}:${port}/mcp`,
|
|
1537
|
+
started: Date.now(),
|
|
1538
|
+
}));
|
|
1539
|
+
} catch {}
|
|
1540
|
+
};
|
|
1541
|
+
|
|
1542
|
+
switch (sub) {
|
|
1543
|
+
case "start": {
|
|
1544
|
+
// 이미 실행 중인지 확인
|
|
1545
|
+
if (existsSync(HUB_PID_FILE)) {
|
|
1546
|
+
try {
|
|
1547
|
+
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1548
|
+
process.kill(info.pid, 0); // 프로세스 존재 확인
|
|
1549
|
+
console.log(`\n ${YELLOW}⚠${RESET} hub 이미 실행 중 (PID ${info.pid}, ${info.url})\n`);
|
|
1550
|
+
return;
|
|
1551
|
+
} catch {
|
|
1552
|
+
// PID 파일 있지만 프로세스 없음 — 정리
|
|
1553
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const portArg = process.argv.indexOf("--port");
|
|
1558
|
+
const port = portArg !== -1 ? process.argv[portArg + 1] : "27888";
|
|
1559
|
+
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
1560
|
+
|
|
1561
|
+
if (!existsSync(serverPath)) {
|
|
1562
|
+
fail("hub/server.mjs 없음 — hub 모듈이 설치되지 않음");
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
1567
|
+
env: { ...process.env, TFX_HUB_PORT: port },
|
|
1568
|
+
stdio: "ignore",
|
|
1569
|
+
detached: true,
|
|
1570
|
+
});
|
|
1571
|
+
child.unref();
|
|
1572
|
+
|
|
1573
|
+
// PID 파일 확인 (최대 3초 대기, 100ms 폴링)
|
|
1574
|
+
let started = false;
|
|
1575
|
+
const deadline = Date.now() + 3000;
|
|
1576
|
+
while (Date.now() < deadline) {
|
|
1577
|
+
if (existsSync(HUB_PID_FILE)) { started = true; break; }
|
|
1578
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
if (started) {
|
|
1582
|
+
const hubInfo = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1583
|
+
console.log(`\n ${GREEN_BRIGHT}✓${RESET} ${BOLD}tfx-hub 시작${RESET}`);
|
|
1584
|
+
console.log(` URL: ${AMBER}${hubInfo.url}${RESET}`);
|
|
1585
|
+
console.log(` PID: ${hubInfo.pid}`);
|
|
1586
|
+
console.log(` DB: ${DIM}${HUB_PID_DIR}/state.db${RESET}`);
|
|
1587
|
+
console.log("");
|
|
1588
|
+
autoRegisterMcp(hubInfo.url);
|
|
1589
|
+
console.log("");
|
|
1590
|
+
} else {
|
|
1591
|
+
// 직접 포그라운드 모드로 안내
|
|
1592
|
+
console.log(`\n ${YELLOW}⚠${RESET} 백그라운드 시작 실패 — 포그라운드로 실행:`);
|
|
1593
|
+
console.log(` ${DIM}TFX_HUB_PORT=${port} node ${serverPath}${RESET}\n`);
|
|
1594
|
+
}
|
|
1595
|
+
break;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
case "stop": {
|
|
1599
|
+
if (!existsSync(HUB_PID_FILE)) {
|
|
1600
|
+
const probed = await probeHubStatus("127.0.0.1", probePort, 1500)
|
|
1601
|
+
|| (probePort === 27888 ? null : await probeHubStatus("127.0.0.1", 27888, 1500));
|
|
1602
|
+
if (probed && Number.isFinite(Number(probed.pid))) {
|
|
1603
|
+
try {
|
|
1604
|
+
process.kill(Number(probed.pid), "SIGTERM");
|
|
1605
|
+
console.log(`\n ${GREEN_BRIGHT}✓${RESET} hub 종료됨 (PID ${probed.pid})${DIM} (probe)${RESET}\n`);
|
|
1606
|
+
return;
|
|
1607
|
+
} catch {}
|
|
1608
|
+
}
|
|
1609
|
+
console.log(`\n ${DIM}hub 미실행${RESET}\n`);
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
try {
|
|
1613
|
+
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1614
|
+
process.kill(info.pid, "SIGTERM");
|
|
1615
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1616
|
+
console.log(`\n ${GREEN_BRIGHT}✓${RESET} hub 종료됨 (PID ${info.pid})\n`);
|
|
1617
|
+
} catch (e) {
|
|
1618
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1619
|
+
console.log(`\n ${DIM}hub 프로세스 없음 — PID 파일 정리됨${RESET}\n`);
|
|
1620
|
+
}
|
|
1621
|
+
break;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
case "status": {
|
|
1625
|
+
if (!existsSync(HUB_PID_FILE)) {
|
|
1626
|
+
const probed = await probeHubStatus();
|
|
1627
|
+
if (!probed) {
|
|
1628
|
+
const fallback = probePort === 27888 ? null : await probeHubStatus("127.0.0.1", 27888, 1500);
|
|
1629
|
+
if (fallback) {
|
|
1630
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(default port probe 성공)${RESET}`);
|
|
1631
|
+
console.log(` URL: http://127.0.0.1:${fallback.port || 27888}/mcp`);
|
|
1632
|
+
if (fallback.pid !== undefined) console.log(` PID: ${fallback.pid}`);
|
|
1633
|
+
if (fallback.hub?.state) console.log(` State: ${fallback.hub.state}`);
|
|
1634
|
+
if (fallback.sessions !== undefined) console.log(` Sessions: ${fallback.sessions}`);
|
|
1635
|
+
recoverPidFile(fallback, "127.0.0.1");
|
|
1636
|
+
console.log("");
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${RED}offline${RESET}\n`);
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(pid file 없음 / probe 성공)${RESET}`);
|
|
1643
|
+
console.log(` URL: http://127.0.0.1:${probed.port || probePort}/mcp`);
|
|
1644
|
+
if (probed.pid !== undefined) console.log(` PID: ${probed.pid}`);
|
|
1645
|
+
if (probed.hub?.state) console.log(` State: ${probed.hub.state}`);
|
|
1646
|
+
if (probed.sessions !== undefined) console.log(` Sessions: ${probed.sessions}`);
|
|
1647
|
+
recoverPidFile(probed, "127.0.0.1");
|
|
1648
|
+
console.log("");
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
try {
|
|
1652
|
+
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1653
|
+
process.kill(info.pid, 0); // 생존 확인
|
|
1654
|
+
const uptime = Date.now() - info.started;
|
|
1655
|
+
const uptimeStr = uptime < 60000 ? `${Math.round(uptime / 1000)}초`
|
|
1656
|
+
: uptime < 3600000 ? `${Math.round(uptime / 60000)}분`
|
|
1657
|
+
: `${Math.round(uptime / 3600000)}시간`;
|
|
1658
|
+
|
|
1659
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET}`);
|
|
1660
|
+
console.log(` URL: ${info.url}`);
|
|
1661
|
+
console.log(` PID: ${info.pid}`);
|
|
1662
|
+
console.log(` Uptime: ${uptimeStr}`);
|
|
1663
|
+
|
|
1664
|
+
// HTTP 상태 조회 시도
|
|
1665
|
+
try {
|
|
1666
|
+
const host = typeof info.host === "string" ? info.host : "127.0.0.1";
|
|
1667
|
+
const port = Number(info.port) || probePort;
|
|
1668
|
+
const data = await probeHubStatus(host, port, 3000);
|
|
1669
|
+
if (data.hub) {
|
|
1670
|
+
console.log(` State: ${data.hub.state}`);
|
|
1671
|
+
}
|
|
1672
|
+
if (data.sessions !== undefined) {
|
|
1673
|
+
console.log(` Sessions: ${data.sessions}`);
|
|
1674
|
+
}
|
|
1675
|
+
} catch {}
|
|
1676
|
+
|
|
1677
|
+
console.log("");
|
|
1678
|
+
} catch {
|
|
1679
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1680
|
+
const probed = await probeHubStatus();
|
|
1681
|
+
if (!probed) {
|
|
1682
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${RED}offline${RESET} ${DIM}(stale PID 정리됨)${RESET}\n`);
|
|
1683
|
+
break;
|
|
1684
|
+
}
|
|
1685
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(stale PID 정리 후 probe 성공)${RESET}`);
|
|
1686
|
+
console.log(` URL: http://127.0.0.1:${probed.port || probePort}/mcp`);
|
|
1687
|
+
if (probed.pid !== undefined) console.log(` PID: ${probed.pid}`);
|
|
1688
|
+
if (probed.hub?.state) console.log(` State: ${probed.hub.state}`);
|
|
1689
|
+
if (probed.sessions !== undefined) console.log(` Sessions: ${probed.sessions}`);
|
|
1690
|
+
recoverPidFile(probed, "127.0.0.1");
|
|
1691
|
+
console.log("");
|
|
1692
|
+
}
|
|
1693
|
+
break;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
default:
|
|
1697
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET}\n`);
|
|
1698
|
+
console.log(` ${WHITE_BRIGHT}tfx hub start${RESET} ${GRAY}허브 데몬 시작${RESET}`);
|
|
1699
|
+
console.log(` ${DIM} --port N${RESET} ${GRAY}포트 지정 (기본 27888)${RESET}`);
|
|
1700
|
+
console.log(` ${WHITE_BRIGHT}tfx hub stop${RESET} ${GRAY}허브 중지${RESET}`);
|
|
1701
|
+
console.log(` ${WHITE_BRIGHT}tfx hub status${RESET} ${GRAY}상태 확인${RESET}\n`);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
// ── 메인 ──
|
|
1706
|
+
|
|
1707
|
+
const cmd = process.argv[2] || "help";
|
|
1708
|
+
|
|
1709
|
+
switch (cmd) {
|
|
1710
|
+
case "setup": cmdSetup(); break;
|
|
1582
1711
|
case "doctor": {
|
|
1583
1712
|
const fix = process.argv.includes("--fix");
|
|
1584
1713
|
const reset = process.argv.includes("--reset");
|
|
1585
1714
|
await cmdDoctor({ fix, reset });
|
|
1586
1715
|
break;
|
|
1587
1716
|
}
|
|
1588
|
-
case "update": cmdUpdate(); break;
|
|
1589
|
-
case "list": case "ls": cmdList(); break;
|
|
1590
|
-
case "hub": await cmdHub(); break;
|
|
1591
|
-
case "
|
|
1592
|
-
|
|
1593
|
-
const {
|
|
1594
|
-
await
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
case "
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1717
|
+
case "update": cmdUpdate(); break;
|
|
1718
|
+
case "list": case "ls": cmdList(); break;
|
|
1719
|
+
case "hub": await cmdHub(); break;
|
|
1720
|
+
case "multi": {
|
|
1721
|
+
await checkHubRunning();
|
|
1722
|
+
const { pathToFileURL } = await import("node:url");
|
|
1723
|
+
const { cmdTeam } = await import(pathToFileURL(join(PKG_ROOT, "hub", "team", "cli.mjs")).href);
|
|
1724
|
+
await cmdTeam();
|
|
1725
|
+
break;
|
|
1726
|
+
}
|
|
1727
|
+
case "codex-team":
|
|
1728
|
+
await checkHubRunning();
|
|
1729
|
+
await cmdCodexTeam();
|
|
1730
|
+
break;
|
|
1731
|
+
case "notion-read": case "nr": {
|
|
1732
|
+
const scriptPath = join(PKG_ROOT, "scripts", "notion-read.mjs");
|
|
1733
|
+
try {
|
|
1734
|
+
execFileSync(process.execPath, [scriptPath, ...process.argv.slice(3)], { stdio: "inherit", timeout: 660000 });
|
|
1735
|
+
} catch (e) { process.exit(e.status || 1); }
|
|
1736
|
+
break;
|
|
1737
|
+
}
|
|
1738
|
+
case "version": case "--version": case "-v": cmdVersion(); break;
|
|
1739
|
+
case "help": case "--help": case "-h": cmdHelp(); break;
|
|
1740
|
+
default:
|
|
1741
|
+
console.error(`알 수 없는 명령: ${cmd}`);
|
|
1742
|
+
cmdHelp();
|
|
1743
|
+
process.exit(1);
|
|
1744
|
+
}
|