triflux 3.2.0-dev.8 → 3.2.0-dev.9
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 +581 -340
- package/hooks/keyword-rules.json +16 -0
- package/hub/bridge.mjs +410 -318
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -0
- package/hub/router.mjs +422 -161
- package/hub/server.mjs +429 -424
- package/hub/store.mjs +388 -314
- package/hub/team/cli-team-common.mjs +348 -0
- package/hub/team/cli-team-control.mjs +393 -0
- package/hub/team/cli-team-start.mjs +512 -0
- package/hub/team/cli-team-status.mjs +269 -0
- package/hub/team/cli.mjs +59 -1459
- package/hub/team/dashboard.mjs +1 -9
- package/hub/team/native.mjs +12 -80
- package/hub/team/nativeProxy.mjs +121 -47
- package/hub/team/pane.mjs +66 -43
- package/hub/team/psmux.mjs +297 -0
- package/hub/team/session.mjs +354 -291
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +41 -52
- package/hub/workers/claude-worker.mjs +446 -0
- package/hub/workers/codex-mcp.mjs +414 -0
- package/hub/workers/factory.mjs +18 -0
- package/hub/workers/gemini-worker.mjs +349 -0
- package/hub/workers/interface.mjs +41 -0
- package/hud/hud-qos-status.mjs +4 -2
- package/package.json +4 -1
- package/scripts/keyword-detector.mjs +15 -0
- package/scripts/lib/keyword-rules.mjs +4 -1
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/setup.mjs +128 -70
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +415 -80
- package/skills/tfx-auto/SKILL.md +90 -564
- package/skills/tfx-auto-codex/SKILL.md +1 -3
- package/skills/tfx-codex/SKILL.md +1 -4
- package/skills/tfx-doctor/SKILL.md +1 -0
- package/skills/tfx-gemini/SKILL.md +1 -4
- package/skills/tfx-setup/SKILL.md +1 -4
- package/skills/tfx-team/SKILL.md +53 -62
package/bin/triflux.mjs
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
// triflux CLI — setup, doctor, version
|
|
3
|
-
import { copyFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync } from "fs";
|
|
4
|
-
import { join, dirname } from "path";
|
|
5
|
-
import { homedir } from "os";
|
|
3
|
+
import { copyFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync, rmSync, statSync } from "fs";
|
|
4
|
+
import { join, dirname } from "path";
|
|
5
|
+
import { homedir } from "os";
|
|
6
6
|
import { execSync, spawn } from "child_process";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
9
|
+
import { detectMultiplexer, getSessionAttachedCount, killSession, listSessions, tmuxExec } from "../hub/team/session.mjs";
|
|
10
|
+
import { cleanupStaleOmcTeams, inspectStaleOmcTeams } from "../hub/team/staleState.mjs";
|
|
11
|
+
|
|
12
|
+
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
10
13
|
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
11
14
|
const CODEX_DIR = join(homedir(), ".codex");
|
|
12
15
|
const CODEX_CONFIG_PATH = join(CODEX_DIR, "config.toml");
|
|
@@ -46,9 +49,10 @@ const RED_BRIGHT = "\x1b[38;5;196m";
|
|
|
46
49
|
|
|
47
50
|
// ── 브랜드 요소 ──
|
|
48
51
|
const BRAND = `${AMBER}${BOLD}triflux${RESET}`;
|
|
49
|
-
const VER = `${DIM}v${PKG.version}${RESET}`;
|
|
50
|
-
const LINE = `${GRAY}${"─".repeat(48)}${RESET}`;
|
|
51
|
-
const DOT = `${GRAY}·${RESET}`;
|
|
52
|
+
const VER = `${DIM}v${PKG.version}${RESET}`;
|
|
53
|
+
const LINE = `${GRAY}${"─".repeat(48)}${RESET}`;
|
|
54
|
+
const DOT = `${GRAY}·${RESET}`;
|
|
55
|
+
const STALE_TEAM_MAX_AGE_SEC = 3600;
|
|
52
56
|
|
|
53
57
|
// ── 유틸리티 ──
|
|
54
58
|
|
|
@@ -68,7 +72,7 @@ function which(cmd) {
|
|
|
68
72
|
} catch { return null; }
|
|
69
73
|
}
|
|
70
74
|
|
|
71
|
-
function whichInShell(cmd, shell) {
|
|
75
|
+
function whichInShell(cmd, shell) {
|
|
72
76
|
const cmds = {
|
|
73
77
|
bash: `bash -c "source ~/.bashrc 2>/dev/null && command -v ${cmd} 2>/dev/null"`,
|
|
74
78
|
cmd: `cmd /c where ${cmd} 2>nul`,
|
|
@@ -83,12 +87,12 @@ function whichInShell(cmd, shell) {
|
|
|
83
87
|
stdio: ["pipe", "pipe", "ignore"],
|
|
84
88
|
}).trim();
|
|
85
89
|
return result.split(/\r?\n/)[0] || null;
|
|
86
|
-
} catch { return null; }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function isDevUpdateRequested(argv = process.argv) {
|
|
90
|
-
return argv.includes("--dev") || argv.includes("@dev") || argv.includes("dev");
|
|
91
|
-
}
|
|
90
|
+
} catch { return null; }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isDevUpdateRequested(argv = process.argv) {
|
|
94
|
+
return argv.includes("--dev") || argv.includes("@dev") || argv.includes("dev");
|
|
95
|
+
}
|
|
92
96
|
|
|
93
97
|
function checkShellAvailable(shell) {
|
|
94
98
|
const cmds = { bash: "bash --version", cmd: "cmd /c echo ok", pwsh: "pwsh -NoProfile -c echo ok" };
|
|
@@ -98,13 +102,136 @@ function checkShellAvailable(shell) {
|
|
|
98
102
|
} catch { return false; }
|
|
99
103
|
}
|
|
100
104
|
|
|
101
|
-
function getVersion(filePath) {
|
|
102
|
-
try {
|
|
103
|
-
const content = readFileSync(filePath, "utf8");
|
|
104
|
-
const match = content.match(/VERSION\s*=\s*"([^"]+)"/);
|
|
105
|
+
function getVersion(filePath) {
|
|
106
|
+
try {
|
|
107
|
+
const content = readFileSync(filePath, "utf8");
|
|
108
|
+
const match = content.match(/VERSION\s*=\s*"([^"]+)"/);
|
|
105
109
|
return match ? match[1] : null;
|
|
106
|
-
} catch { return null; }
|
|
107
|
-
}
|
|
110
|
+
} catch { return null; }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseSessionCreated(rawValue) {
|
|
114
|
+
const value = String(rawValue || "").trim();
|
|
115
|
+
if (!value) return null;
|
|
116
|
+
|
|
117
|
+
const numeric = Number(value);
|
|
118
|
+
if (Number.isFinite(numeric) && numeric > 0) {
|
|
119
|
+
return numeric > 1e12 ? Math.floor(numeric / 1000) : Math.floor(numeric);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const parsed = Date.parse(value);
|
|
123
|
+
if (Number.isFinite(parsed)) {
|
|
124
|
+
return Math.floor(parsed / 1000);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const normalized = value.replace(/^(\d{2})-(\d{2})-(\d{2})(\s+)/, "20$1-$2-$3$4");
|
|
128
|
+
const reparsed = Date.parse(normalized);
|
|
129
|
+
if (Number.isFinite(reparsed)) {
|
|
130
|
+
return Math.floor(reparsed / 1000);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatElapsedAge(ageSec) {
|
|
137
|
+
if (!Number.isFinite(ageSec) || ageSec < 0) return "알 수 없음";
|
|
138
|
+
if (ageSec < 60) return `${ageSec}초`;
|
|
139
|
+
if (ageSec < 3600) return `${Math.floor(ageSec / 60)}분`;
|
|
140
|
+
if (ageSec < 86400) return `${Math.floor(ageSec / 3600)}시간`;
|
|
141
|
+
return `${Math.floor(ageSec / 86400)}일`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readTeamSessionCreatedMap() {
|
|
145
|
+
const createdMap = new Map();
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const output = tmuxExec('list-sessions -F "#{session_name} #{session_created}"');
|
|
149
|
+
for (const line of output.split(/\r?\n/)) {
|
|
150
|
+
const trimmed = line.trim();
|
|
151
|
+
if (!trimmed) continue;
|
|
152
|
+
|
|
153
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
154
|
+
if (firstSpace === -1) continue;
|
|
155
|
+
|
|
156
|
+
const sessionName = trimmed.slice(0, firstSpace);
|
|
157
|
+
const createdRaw = trimmed.slice(firstSpace + 1).trim();
|
|
158
|
+
const createdAt = parseSessionCreated(createdRaw);
|
|
159
|
+
createdMap.set(sessionName, {
|
|
160
|
+
createdAt,
|
|
161
|
+
createdRaw,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// session_created 포맷을 읽지 못하면 stale 판정만 완화한다.
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return createdMap;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function inspectTeamSessions() {
|
|
172
|
+
const mux = detectMultiplexer();
|
|
173
|
+
if (!mux) {
|
|
174
|
+
return { mux: null, sessions: [] };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const sessionNames = listSessions();
|
|
178
|
+
if (sessionNames.length === 0) {
|
|
179
|
+
return { mux, sessions: [] };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const createdMap = readTeamSessionCreatedMap();
|
|
183
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
184
|
+
const sessions = sessionNames.map((sessionName) => {
|
|
185
|
+
const createdInfo = createdMap.get(sessionName) || { createdAt: null, createdRaw: "" };
|
|
186
|
+
const attachedCount = getSessionAttachedCount(sessionName);
|
|
187
|
+
const ageSec = createdInfo.createdAt == null ? null : Math.max(0, nowSec - createdInfo.createdAt);
|
|
188
|
+
const stale = ageSec != null && ageSec >= STALE_TEAM_MAX_AGE_SEC && attachedCount === 0;
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
sessionName,
|
|
192
|
+
attachedCount,
|
|
193
|
+
ageSec,
|
|
194
|
+
createdAt: createdInfo.createdAt,
|
|
195
|
+
createdRaw: createdInfo.createdRaw,
|
|
196
|
+
stale,
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return { mux, sessions };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function cleanupStaleTeamSessions(staleSessions) {
|
|
204
|
+
let cleaned = 0;
|
|
205
|
+
let failed = 0;
|
|
206
|
+
|
|
207
|
+
for (const session of staleSessions) {
|
|
208
|
+
let removed = false;
|
|
209
|
+
|
|
210
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
211
|
+
killSession(session.sessionName);
|
|
212
|
+
const stillAlive = listSessions().includes(session.sessionName);
|
|
213
|
+
if (!stillAlive) {
|
|
214
|
+
removed = true;
|
|
215
|
+
cleaned++;
|
|
216
|
+
ok(`stale 세션 정리: ${session.sessionName}`);
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (attempt < 3) {
|
|
221
|
+
await delay(1000);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (!removed) {
|
|
226
|
+
failed++;
|
|
227
|
+
fail(`세션 정리 실패: ${session.sessionName} — 수동 정리 필요`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
info(`${cleaned}개 stale 세션 정리 완료`);
|
|
232
|
+
|
|
233
|
+
return { cleaned, failed };
|
|
234
|
+
}
|
|
108
235
|
|
|
109
236
|
function escapeRegExp(value) {
|
|
110
237
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -343,10 +470,10 @@ function cmdSetup() {
|
|
|
343
470
|
console.log(`\n${DIM}설치 위치: ${CLAUDE_DIR}${RESET}\n`);
|
|
344
471
|
}
|
|
345
472
|
|
|
346
|
-
function cmdDoctor(options = {}) {
|
|
347
|
-
const { fix = false, reset = false } = options;
|
|
348
|
-
const modeLabel = reset ? ` ${RED}--reset${RESET}` : fix ? ` ${YELLOW}--fix${RESET}` : "";
|
|
349
|
-
console.log(`\n ${AMBER}${BOLD}⬡ triflux doctor${RESET} ${VER}${modeLabel}\n`);
|
|
473
|
+
async function cmdDoctor(options = {}) {
|
|
474
|
+
const { fix = false, reset = false } = options;
|
|
475
|
+
const modeLabel = reset ? ` ${RED}--reset${RESET}` : fix ? ` ${YELLOW}--fix${RESET}` : "";
|
|
476
|
+
console.log(`\n ${AMBER}${BOLD}⬡ triflux doctor${RESET} ${VER}${modeLabel}\n`);
|
|
350
477
|
console.log(` ${LINE}`);
|
|
351
478
|
|
|
352
479
|
// ── reset 모드: 캐시 전체 초기화 ──
|
|
@@ -431,10 +558,10 @@ function cmdDoctor(options = {}) {
|
|
|
431
558
|
// 스킬 동기화
|
|
432
559
|
const fSkillsSrc = join(PKG_ROOT, "skills");
|
|
433
560
|
const fSkillsDst = join(CLAUDE_DIR, "skills");
|
|
434
|
-
if (existsSync(fSkillsSrc)) {
|
|
435
|
-
let sc = 0, st = 0;
|
|
436
|
-
for (const name of readdirSync(fSkillsSrc)) {
|
|
437
|
-
const src = join(fSkillsSrc, name, "SKILL.md");
|
|
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");
|
|
438
565
|
const dst = join(fSkillsDst, name, "SKILL.md");
|
|
439
566
|
if (!existsSync(src)) continue;
|
|
440
567
|
st++;
|
|
@@ -443,21 +570,21 @@ function cmdDoctor(options = {}) {
|
|
|
443
570
|
if (!existsSync(dst)) { copyFileSync(src, dst); sc++; }
|
|
444
571
|
else if (readFileSync(src, "utf8") !== readFileSync(dst, "utf8")) { copyFileSync(src, dst); sc++; }
|
|
445
572
|
}
|
|
446
|
-
if (sc > 0) ok(`스킬: ${sc}/${st}개 업데이트됨`);
|
|
447
|
-
else ok(`스킬: ${st}개 최신 상태`);
|
|
448
|
-
}
|
|
449
|
-
const profileFix = ensureCodexProfiles();
|
|
450
|
-
if (!profileFix.ok) {
|
|
451
|
-
warn(`Codex Profiles 자동 복구 실패: ${profileFix.message}`);
|
|
452
|
-
} else if (profileFix.added > 0) {
|
|
453
|
-
ok(`Codex Profiles: ${profileFix.added}개 추가됨`);
|
|
454
|
-
} else {
|
|
455
|
-
info("Codex Profiles: 이미 최신 상태");
|
|
456
|
-
}
|
|
457
|
-
// 에러/스테일 캐시 정리
|
|
458
|
-
const fCacheDir = join(CLAUDE_DIR, "cache");
|
|
459
|
-
const staleNames = ["claude-usage-cache.json", ".claude-refresh-lock", "codex-rate-limits-cache.json"];
|
|
460
|
-
let cleaned = 0;
|
|
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;
|
|
461
588
|
for (const name of staleNames) {
|
|
462
589
|
const fp = join(fCacheDir, name);
|
|
463
590
|
if (!existsSync(fp)) continue;
|
|
@@ -497,56 +624,56 @@ function cmdDoctor(options = {}) {
|
|
|
497
624
|
// 3. Codex CLI
|
|
498
625
|
section(`Codex CLI ${WHITE_BRIGHT}●${RESET}`);
|
|
499
626
|
issues += checkCliCrossShell("codex", "npm install -g @openai/codex");
|
|
500
|
-
if (which("codex")) {
|
|
501
|
-
if (process.env.OPENAI_API_KEY) {
|
|
502
|
-
ok("OPENAI_API_KEY 설정됨");
|
|
503
|
-
} else {
|
|
504
|
-
warn(`OPENAI_API_KEY 미설정 ${GRAY}(Pro 구독이면 불필요)${RESET}`);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// 4. Codex Profiles
|
|
509
|
-
section("Codex Profiles");
|
|
510
|
-
if (existsSync(CODEX_CONFIG_PATH)) {
|
|
511
|
-
const codexConfig = readFileSync(CODEX_CONFIG_PATH, "utf8");
|
|
512
|
-
for (const profile of REQUIRED_CODEX_PROFILES) {
|
|
513
|
-
if (hasProfileSection(codexConfig, profile.name)) {
|
|
514
|
-
ok(`${profile.name}: 정상`);
|
|
515
|
-
} else {
|
|
516
|
-
warn(`${profile.name}: 미설정`);
|
|
517
|
-
issues++;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
} else {
|
|
521
|
-
warn("config.toml 미존재");
|
|
522
|
-
issues++;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// 5. Gemini CLI
|
|
526
|
-
section(`Gemini CLI ${BLUE}●${RESET}`);
|
|
527
|
-
issues += checkCliCrossShell("gemini", "npm install -g @google/gemini-cli");
|
|
528
|
-
if (which("gemini")) {
|
|
529
|
-
if (process.env.GEMINI_API_KEY) {
|
|
530
|
-
ok("GEMINI_API_KEY 설정됨");
|
|
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 설정됨");
|
|
531
658
|
} else {
|
|
532
659
|
warn(`GEMINI_API_KEY 미설정 ${GRAY}(gemini auth login)${RESET}`);
|
|
533
660
|
}
|
|
534
661
|
}
|
|
535
662
|
|
|
536
|
-
// 6. Claude Code
|
|
537
|
-
section(`Claude Code ${AMBER}●${RESET}`);
|
|
538
|
-
const claudePath = which("claude");
|
|
539
|
-
if (claudePath) {
|
|
540
|
-
ok("설치됨");
|
|
541
|
-
} else {
|
|
663
|
+
// 6. Claude Code
|
|
664
|
+
section(`Claude Code ${AMBER}●${RESET}`);
|
|
665
|
+
const claudePath = which("claude");
|
|
666
|
+
if (claudePath) {
|
|
667
|
+
ok("설치됨");
|
|
668
|
+
} else {
|
|
542
669
|
fail("미설치 (필수)");
|
|
543
670
|
issues++;
|
|
544
671
|
}
|
|
545
672
|
|
|
546
|
-
// 7. 스킬 설치 상태
|
|
547
|
-
section("Skills");
|
|
548
|
-
const skillsSrc = join(PKG_ROOT, "skills");
|
|
549
|
-
const skillsDst = join(CLAUDE_DIR, "skills");
|
|
673
|
+
// 7. 스킬 설치 상태
|
|
674
|
+
section("Skills");
|
|
675
|
+
const skillsSrc = join(PKG_ROOT, "skills");
|
|
676
|
+
const skillsDst = join(CLAUDE_DIR, "skills");
|
|
550
677
|
if (existsSync(skillsSrc)) {
|
|
551
678
|
let installed = 0;
|
|
552
679
|
let total = 0;
|
|
@@ -569,9 +696,9 @@ function cmdDoctor(options = {}) {
|
|
|
569
696
|
}
|
|
570
697
|
}
|
|
571
698
|
|
|
572
|
-
// 8. 플러그인 등록
|
|
573
|
-
section("Plugin");
|
|
574
|
-
const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
|
|
699
|
+
// 8. 플러그인 등록
|
|
700
|
+
section("Plugin");
|
|
701
|
+
const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
|
|
575
702
|
if (existsSync(pluginsFile)) {
|
|
576
703
|
const content = readFileSync(pluginsFile, "utf8");
|
|
577
704
|
if (content.includes("triflux")) {
|
|
@@ -584,9 +711,9 @@ function cmdDoctor(options = {}) {
|
|
|
584
711
|
info("플러그인 시스템 감지 안 됨 — npm 단독 사용");
|
|
585
712
|
}
|
|
586
713
|
|
|
587
|
-
// 9. MCP 인벤토리
|
|
588
|
-
section("MCP Inventory");
|
|
589
|
-
const mcpCache = join(CLAUDE_DIR, "cache", "mcp-inventory.json");
|
|
714
|
+
// 9. MCP 인벤토리
|
|
715
|
+
section("MCP Inventory");
|
|
716
|
+
const mcpCache = join(CLAUDE_DIR, "cache", "mcp-inventory.json");
|
|
590
717
|
if (existsSync(mcpCache)) {
|
|
591
718
|
try {
|
|
592
719
|
const inv = JSON.parse(readFileSync(mcpCache, "utf8"));
|
|
@@ -607,8 +734,8 @@ function cmdDoctor(options = {}) {
|
|
|
607
734
|
info(`수동: node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}`);
|
|
608
735
|
}
|
|
609
736
|
|
|
610
|
-
// 10. CLI 이슈 트래커
|
|
611
|
-
section("CLI Issues");
|
|
737
|
+
// 10. CLI 이슈 트래커
|
|
738
|
+
section("CLI Issues");
|
|
612
739
|
const issuesFile = join(CLAUDE_DIR, "cache", "cli-issues.jsonl");
|
|
613
740
|
if (existsSync(issuesFile)) {
|
|
614
741
|
try {
|
|
@@ -672,6 +799,120 @@ function cmdDoctor(options = {}) {
|
|
|
672
799
|
ok("이슈 로그 없음 (정상)");
|
|
673
800
|
}
|
|
674
801
|
|
|
802
|
+
// 11. Team Sessions
|
|
803
|
+
section("Team Sessions");
|
|
804
|
+
const teamSessionReport = inspectTeamSessions();
|
|
805
|
+
if (!teamSessionReport.mux) {
|
|
806
|
+
info("tmux/psmux 미감지 — 팀 세션 검사 건너뜀");
|
|
807
|
+
} else if (teamSessionReport.sessions.length === 0) {
|
|
808
|
+
ok(`활성 팀 세션 없음 ${DIM}(${teamSessionReport.mux})${RESET}`);
|
|
809
|
+
} else {
|
|
810
|
+
info(`multiplexer: ${teamSessionReport.mux}`);
|
|
811
|
+
|
|
812
|
+
for (const session of teamSessionReport.sessions) {
|
|
813
|
+
const attachedLabel = session.attachedCount == null ? "?" : `${session.attachedCount}`;
|
|
814
|
+
const ageLabel = formatElapsedAge(session.ageSec);
|
|
815
|
+
|
|
816
|
+
if (session.stale) {
|
|
817
|
+
warn(`${session.sessionName}: stale 추정 (attach=${attachedLabel}, 경과=${ageLabel})`);
|
|
818
|
+
} else {
|
|
819
|
+
ok(`${session.sessionName}: 정상 (attach=${attachedLabel}, 경과=${ageLabel})`);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (session.createdAt == null) {
|
|
823
|
+
info(`${session.sessionName}: session_created 파싱 실패${session.createdRaw ? ` (${session.createdRaw})` : ""}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const staleSessions = teamSessionReport.sessions.filter((session) => session.stale);
|
|
828
|
+
if (staleSessions.length > 0) {
|
|
829
|
+
if (fix) {
|
|
830
|
+
const cleanupResult = await cleanupStaleTeamSessions(staleSessions);
|
|
831
|
+
issues += cleanupResult.failed;
|
|
832
|
+
} else {
|
|
833
|
+
info("정리: tfx doctor --fix");
|
|
834
|
+
issues += staleSessions.length;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// 12. OMC stale team 상태
|
|
840
|
+
section("OMC Stale Teams");
|
|
841
|
+
const omcTeamReport = inspectStaleOmcTeams({
|
|
842
|
+
startDir: process.cwd(),
|
|
843
|
+
maxAgeMs: STALE_TEAM_MAX_AGE_SEC * 1000,
|
|
844
|
+
liveSessionNames: teamSessionReport.sessions.map((session) => session.sessionName),
|
|
845
|
+
});
|
|
846
|
+
if (!omcTeamReport.stateRoot) {
|
|
847
|
+
info(".omc/state 없음 — 검사 건너뜀");
|
|
848
|
+
} else if (omcTeamReport.entries.length === 0) {
|
|
849
|
+
ok(`stale team 없음 ${DIM}(${omcTeamReport.stateRoot})${RESET}`);
|
|
850
|
+
} else {
|
|
851
|
+
warn(`${omcTeamReport.entries.length}개 stale team 발견`);
|
|
852
|
+
|
|
853
|
+
for (const entry of omcTeamReport.entries) {
|
|
854
|
+
const ageLabel = formatElapsedAge(entry.ageSec);
|
|
855
|
+
const scopeLabel = entry.scope === "root" ? "root-state" : entry.sessionId;
|
|
856
|
+
warn(`${scopeLabel}: stale team (경과=${ageLabel}, 프로세스 없음)`);
|
|
857
|
+
if (entry.teamName) info(`팀: ${entry.teamName}`);
|
|
858
|
+
info(`파일: ${entry.stateFile}`);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (fix) {
|
|
862
|
+
const cleanupResult = cleanupStaleOmcTeams(omcTeamReport.entries);
|
|
863
|
+
for (const result of cleanupResult.results) {
|
|
864
|
+
if (result.ok) {
|
|
865
|
+
ok(`stale team 정리: ${result.entry.scope === "root" ? "root-state" : result.entry.sessionId}`);
|
|
866
|
+
} else {
|
|
867
|
+
fail(`stale team 정리 실패: ${result.entry.scope === "root" ? "root-state" : result.entry.sessionId} — ${result.error.message}`);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
issues += cleanupResult.failed;
|
|
871
|
+
} else {
|
|
872
|
+
info("정리: tfx doctor --fix");
|
|
873
|
+
issues += omcTeamReport.entries.length;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// 13. Orphan Teams
|
|
878
|
+
section("Orphan Teams");
|
|
879
|
+
const teamsDir = join(CLAUDE_DIR, "teams");
|
|
880
|
+
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
|
+
warn(`${teamDirs.length}개 잔존 팀 발견: ${teamDirs.join(", ")}`);
|
|
890
|
+
if (fix) {
|
|
891
|
+
let cleaned = 0;
|
|
892
|
+
for (const d of teamDirs) {
|
|
893
|
+
try {
|
|
894
|
+
rmSync(join(teamsDir, d), { recursive: true, force: true });
|
|
895
|
+
cleaned++;
|
|
896
|
+
} catch {}
|
|
897
|
+
// 연관 tasks 디렉토리도 정리
|
|
898
|
+
const taskDir = join(tasksDir, d);
|
|
899
|
+
if (existsSync(taskDir)) {
|
|
900
|
+
try { rmSync(taskDir, { recursive: true, force: true }); } catch {}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
ok(`${cleaned}개 잔존 팀 정리 완료`);
|
|
904
|
+
} else {
|
|
905
|
+
info("정리: /tfx-doctor --fix 또는 수동 rm -rf ~/.claude/teams/{name}/");
|
|
906
|
+
issues++;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
} catch (e) {
|
|
910
|
+
warn(`teams 디렉토리 읽기 실패: ${e.message}`);
|
|
911
|
+
}
|
|
912
|
+
} else {
|
|
913
|
+
ok("잔존 팀 없음");
|
|
914
|
+
}
|
|
915
|
+
|
|
675
916
|
// 결과
|
|
676
917
|
console.log(`\n ${LINE}`);
|
|
677
918
|
if (issues === 0) {
|
|
@@ -681,10 +922,10 @@ function cmdDoctor(options = {}) {
|
|
|
681
922
|
}
|
|
682
923
|
}
|
|
683
924
|
|
|
684
|
-
function cmdUpdate() {
|
|
685
|
-
const isDev = isDevUpdateRequested(process.argv);
|
|
686
|
-
const tagLabel = isDev ? ` ${YELLOW}--dev${RESET}` : "";
|
|
687
|
-
console.log(`\n${BOLD}triflux update${RESET}${tagLabel}\n`);
|
|
925
|
+
function cmdUpdate() {
|
|
926
|
+
const isDev = isDevUpdateRequested(process.argv);
|
|
927
|
+
const tagLabel = isDev ? ` ${YELLOW}--dev${RESET}` : "";
|
|
928
|
+
console.log(`\n${BOLD}triflux update${RESET}${tagLabel}\n`);
|
|
688
929
|
|
|
689
930
|
// 1. 설치 방식 감지
|
|
690
931
|
const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
|
|
@@ -736,13 +977,13 @@ function cmdUpdate() {
|
|
|
736
977
|
|
|
737
978
|
info(`검색: ${installMode === "plugin" ? "플러그인" : installMode === "npm-global" ? "npm global" : installMode === "npm-local" ? "npm local" : installMode === "git-local" ? "git 로컬 저장소" : "알 수 없음"} 설치 감지`);
|
|
738
979
|
|
|
739
|
-
// 2. 설치 방식에 따라 업데이트
|
|
740
|
-
const oldVer = PKG.version;
|
|
741
|
-
let updated = false;
|
|
742
|
-
let stoppedHubInfo = null;
|
|
743
|
-
|
|
744
|
-
try {
|
|
745
|
-
switch (installMode) {
|
|
980
|
+
// 2. 설치 방식에 따라 업데이트
|
|
981
|
+
const oldVer = PKG.version;
|
|
982
|
+
let updated = false;
|
|
983
|
+
let stoppedHubInfo = null;
|
|
984
|
+
|
|
985
|
+
try {
|
|
986
|
+
switch (installMode) {
|
|
746
987
|
case "plugin": {
|
|
747
988
|
const gitDir = pluginPath || PKG_ROOT;
|
|
748
989
|
const result = execSync("git pull", {
|
|
@@ -753,19 +994,19 @@ function cmdUpdate() {
|
|
|
753
994
|
ok(`git pull — ${result}`);
|
|
754
995
|
updated = true;
|
|
755
996
|
break;
|
|
756
|
-
}
|
|
757
|
-
case "npm-global": {
|
|
758
|
-
stoppedHubInfo = stopHubForUpdate();
|
|
759
|
-
if (stoppedHubInfo?.pid) {
|
|
760
|
-
info(`실행 중 hub 정지 (PID ${stoppedHubInfo.pid})`);
|
|
761
|
-
}
|
|
762
|
-
const npmCmd = isDev ? "npm install -g triflux@dev" : "npm update -g triflux";
|
|
763
|
-
const result = execSync(npmCmd, {
|
|
764
|
-
encoding: "utf8",
|
|
997
|
+
}
|
|
998
|
+
case "npm-global": {
|
|
999
|
+
stoppedHubInfo = stopHubForUpdate();
|
|
1000
|
+
if (stoppedHubInfo?.pid) {
|
|
1001
|
+
info(`실행 중 hub 정지 (PID ${stoppedHubInfo.pid})`);
|
|
1002
|
+
}
|
|
1003
|
+
const npmCmd = isDev ? "npm install -g triflux@dev" : "npm update -g triflux";
|
|
1004
|
+
const result = execSync(npmCmd, {
|
|
1005
|
+
encoding: "utf8",
|
|
765
1006
|
timeout: 60000,
|
|
766
1007
|
stdio: ["pipe", "pipe", "ignore"],
|
|
767
1008
|
}).trim().split(/\r?\n/)[0];
|
|
768
|
-
ok(`${isDev ? "npm install -g triflux@dev" : "npm update -g triflux"} — ${result || "완료"}`);
|
|
1009
|
+
ok(`${isDev ? "npm install -g triflux@dev" : "npm update -g triflux"} — ${result || "완료"}`);
|
|
769
1010
|
updated = true;
|
|
770
1011
|
break;
|
|
771
1012
|
}
|
|
@@ -777,7 +1018,7 @@ function cmdUpdate() {
|
|
|
777
1018
|
cwd: process.cwd(),
|
|
778
1019
|
stdio: ["pipe", "pipe", "ignore"],
|
|
779
1020
|
}).trim().split(/\r?\n/)[0];
|
|
780
|
-
ok(`${isDev ? "npm install triflux@dev" : "npm update triflux"} — ${result || "완료"}`);
|
|
1021
|
+
ok(`${isDev ? "npm install triflux@dev" : "npm update triflux"} — ${result || "완료"}`);
|
|
781
1022
|
updated = true;
|
|
782
1023
|
break;
|
|
783
1024
|
}
|
|
@@ -795,14 +1036,14 @@ function cmdUpdate() {
|
|
|
795
1036
|
fail("설치 방식을 감지할 수 없음");
|
|
796
1037
|
info("수동 업데이트: cd <triflux-dir> && git pull");
|
|
797
1038
|
return;
|
|
798
|
-
}
|
|
799
|
-
} catch (e) {
|
|
800
|
-
if (stoppedHubInfo && startHubAfterUpdate(stoppedHubInfo)) {
|
|
801
|
-
info("업데이트 실패 후 hub 재기동 시도");
|
|
802
|
-
}
|
|
803
|
-
fail(`업데이트 실패: ${e.message}`);
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
1039
|
+
}
|
|
1040
|
+
} catch (e) {
|
|
1041
|
+
if (stoppedHubInfo && startHubAfterUpdate(stoppedHubInfo)) {
|
|
1042
|
+
info("업데이트 실패 후 hub 재기동 시도");
|
|
1043
|
+
}
|
|
1044
|
+
fail(`업데이트 실패: ${e.message}`);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
806
1047
|
|
|
807
1048
|
// 3. setup 재실행 (tfx-route.sh, HUD, 스킬 동기화)
|
|
808
1049
|
if (updated) {
|
|
@@ -820,16 +1061,16 @@ function cmdUpdate() {
|
|
|
820
1061
|
ok(`버전: v${oldVer} (이미 최신)`);
|
|
821
1062
|
}
|
|
822
1063
|
|
|
823
|
-
// setup 재실행
|
|
824
|
-
console.log("");
|
|
825
|
-
info("setup 재실행 중...");
|
|
826
|
-
cmdSetup();
|
|
827
|
-
|
|
828
|
-
if (stoppedHubInfo) {
|
|
829
|
-
if (startHubAfterUpdate(stoppedHubInfo)) info("hub 재기동 완료");
|
|
830
|
-
else warn("hub 재기동 실패 — `tfx hub start`로 수동 시작 필요");
|
|
831
|
-
}
|
|
832
|
-
}
|
|
1064
|
+
// setup 재실행
|
|
1065
|
+
console.log("");
|
|
1066
|
+
info("setup 재실행 중...");
|
|
1067
|
+
cmdSetup();
|
|
1068
|
+
|
|
1069
|
+
if (stoppedHubInfo) {
|
|
1070
|
+
if (startHubAfterUpdate(stoppedHubInfo)) info("hub 재기동 완료");
|
|
1071
|
+
else warn("hub 재기동 실패 — `tfx hub start`로 수동 시작 필요");
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
833
1074
|
|
|
834
1075
|
console.log(`${GREEN}${BOLD}업데이트 완료${RESET}\n`);
|
|
835
1076
|
}
|
|
@@ -932,8 +1173,8 @@ ${updateNotice}
|
|
|
932
1173
|
${WHITE_BRIGHT}tfx doctor${RESET} ${GRAY}CLI 진단 + 이슈 확인${RESET}
|
|
933
1174
|
${DIM} --fix${RESET} ${GRAY}진단 + 자동 수정${RESET}
|
|
934
1175
|
${DIM} --reset${RESET} ${GRAY}캐시 전체 초기화${RESET}
|
|
935
|
-
${WHITE_BRIGHT}tfx update${RESET} ${GRAY}최신 안정 버전으로 업데이트${RESET}
|
|
936
|
-
${DIM} --dev / dev${RESET} ${GRAY}dev 태그로 업데이트${RESET}
|
|
1176
|
+
${WHITE_BRIGHT}tfx update${RESET} ${GRAY}최신 안정 버전으로 업데이트${RESET}
|
|
1177
|
+
${DIM} --dev / dev${RESET} ${GRAY}dev 태그로 업데이트${RESET}
|
|
937
1178
|
${WHITE_BRIGHT}tfx list${RESET} ${GRAY}설치된 스킬 목록${RESET}
|
|
938
1179
|
${WHITE_BRIGHT}tfx hub${RESET} ${GRAY}MCP 메시지 버스 관리 (start/stop/status)${RESET}
|
|
939
1180
|
${WHITE_BRIGHT}tfx team${RESET} ${GRAY}멀티-CLI 팀 모드 (tmux + Hub)${RESET}
|
|
@@ -941,12 +1182,12 @@ ${updateNotice}
|
|
|
941
1182
|
${WHITE_BRIGHT}tfx notion-read${RESET} ${GRAY}Notion 페이지 → 마크다운 (Codex/Gemini MCP)${RESET}
|
|
942
1183
|
${WHITE_BRIGHT}tfx version${RESET} ${GRAY}버전 표시${RESET}
|
|
943
1184
|
|
|
944
|
-
${BOLD}Skills${RESET} ${GRAY}(Claude Code 슬래시 커맨드)${RESET}
|
|
945
|
-
|
|
946
|
-
${AMBER}/tfx-auto${RESET} ${GRAY}자동 분류 + 병렬 실행${RESET}
|
|
947
|
-
${WHITE_BRIGHT}/tfx-auto-codex${RESET} ${GRAY}Codex 리드 + Gemini 유지 (no-Claude-native)${RESET}
|
|
948
|
-
${WHITE_BRIGHT}/tfx-codex${RESET} ${GRAY}Codex 전용 모드${RESET}
|
|
949
|
-
${BLUE}/tfx-gemini${RESET} ${GRAY}Gemini 전용 모드${RESET}
|
|
1185
|
+
${BOLD}Skills${RESET} ${GRAY}(Claude Code 슬래시 커맨드)${RESET}
|
|
1186
|
+
|
|
1187
|
+
${AMBER}/tfx-auto${RESET} ${GRAY}자동 분류 + 병렬 실행${RESET}
|
|
1188
|
+
${WHITE_BRIGHT}/tfx-auto-codex${RESET} ${GRAY}Codex 리드 + Gemini 유지 (no-Claude-native)${RESET}
|
|
1189
|
+
${WHITE_BRIGHT}/tfx-codex${RESET} ${GRAY}Codex 전용 모드${RESET}
|
|
1190
|
+
${BLUE}/tfx-gemini${RESET} ${GRAY}Gemini 전용 모드${RESET}
|
|
950
1191
|
${AMBER}/tfx-setup${RESET} ${GRAY}HUD 설정 + 진단${RESET}
|
|
951
1192
|
${YELLOW}/tfx-doctor${RESET} ${GRAY}진단 + 수리 + 캐시 초기화${RESET}
|
|
952
1193
|
|
|
@@ -979,88 +1220,88 @@ async function cmdCodexTeam() {
|
|
|
979
1220
|
return;
|
|
980
1221
|
}
|
|
981
1222
|
|
|
982
|
-
const hasAgents = args.includes("--agents");
|
|
983
|
-
const hasLead = args.includes("--lead");
|
|
984
|
-
const hasLayout = args.includes("--layout");
|
|
985
|
-
const isControl = passthrough.has(sub);
|
|
986
|
-
const normalizedArgs = isControl && args.length ? [sub, ...args.slice(1)] : args;
|
|
987
|
-
const inject = [];
|
|
988
|
-
if (!isControl && !hasLead) inject.push("--lead", "codex");
|
|
989
|
-
if (!isControl && !hasAgents) inject.push("--agents", "codex,codex");
|
|
990
|
-
if (!isControl && !hasLayout) inject.push("--layout", "1xN");
|
|
991
|
-
const forwarded = isControl ? normalizedArgs : [...inject, ...args];
|
|
992
|
-
|
|
993
|
-
const prevArgv = process.argv;
|
|
994
|
-
const prevProfile = process.env.TFX_TEAM_PROFILE;
|
|
995
|
-
process.env.TFX_TEAM_PROFILE = "codex-team";
|
|
996
|
-
const { pathToFileURL } = await import("node:url");
|
|
997
|
-
const { cmdTeam } = await import(pathToFileURL(join(PKG_ROOT, "hub", "team", "cli.mjs")).href);
|
|
998
|
-
process.argv = [prevArgv[0], prevArgv[1], "team", ...forwarded];
|
|
999
|
-
try {
|
|
1000
|
-
await cmdTeam();
|
|
1001
|
-
} finally {
|
|
1002
|
-
process.argv = prevArgv;
|
|
1003
|
-
if (typeof prevProfile === "string") process.env.TFX_TEAM_PROFILE = prevProfile;
|
|
1004
|
-
else delete process.env.TFX_TEAM_PROFILE;
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1223
|
+
const hasAgents = args.includes("--agents");
|
|
1224
|
+
const hasLead = args.includes("--lead");
|
|
1225
|
+
const hasLayout = args.includes("--layout");
|
|
1226
|
+
const isControl = passthrough.has(sub);
|
|
1227
|
+
const normalizedArgs = isControl && args.length ? [sub, ...args.slice(1)] : args;
|
|
1228
|
+
const inject = [];
|
|
1229
|
+
if (!isControl && !hasLead) inject.push("--lead", "codex");
|
|
1230
|
+
if (!isControl && !hasAgents) inject.push("--agents", "codex,codex");
|
|
1231
|
+
if (!isControl && !hasLayout) inject.push("--layout", "1xN");
|
|
1232
|
+
const forwarded = isControl ? normalizedArgs : [...inject, ...args];
|
|
1233
|
+
|
|
1234
|
+
const prevArgv = process.argv;
|
|
1235
|
+
const prevProfile = process.env.TFX_TEAM_PROFILE;
|
|
1236
|
+
process.env.TFX_TEAM_PROFILE = "codex-team";
|
|
1237
|
+
const { pathToFileURL } = await import("node:url");
|
|
1238
|
+
const { cmdTeam } = await import(pathToFileURL(join(PKG_ROOT, "hub", "team", "cli.mjs")).href);
|
|
1239
|
+
process.argv = [prevArgv[0], prevArgv[1], "team", ...forwarded];
|
|
1240
|
+
try {
|
|
1241
|
+
await cmdTeam();
|
|
1242
|
+
} finally {
|
|
1243
|
+
process.argv = prevArgv;
|
|
1244
|
+
if (typeof prevProfile === "string") process.env.TFX_TEAM_PROFILE = prevProfile;
|
|
1245
|
+
else delete process.env.TFX_TEAM_PROFILE;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1007
1248
|
|
|
1008
1249
|
// ── hub 서브커맨드 ──
|
|
1009
1250
|
|
|
1010
|
-
const HUB_PID_DIR = join(homedir(), ".claude", "cache", "tfx-hub");
|
|
1011
|
-
const HUB_PID_FILE = join(HUB_PID_DIR, "hub.pid");
|
|
1012
|
-
|
|
1013
|
-
function sleepMs(ms) {
|
|
1014
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
function stopHubForUpdate() {
|
|
1018
|
-
if (!existsSync(HUB_PID_FILE)) return null;
|
|
1019
|
-
let info = null;
|
|
1020
|
-
try {
|
|
1021
|
-
info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1022
|
-
process.kill(info.pid, 0);
|
|
1023
|
-
} catch {
|
|
1024
|
-
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1025
|
-
return null;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
try {
|
|
1029
|
-
if (process.platform === "win32") {
|
|
1030
|
-
execSync(`taskkill /PID ${info.pid} /T /F`, {
|
|
1031
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
1032
|
-
timeout: 10000,
|
|
1033
|
-
});
|
|
1034
|
-
} else {
|
|
1035
|
-
process.kill(info.pid, "SIGTERM");
|
|
1036
|
-
}
|
|
1037
|
-
} catch {
|
|
1038
|
-
try { process.kill(info.pid, "SIGKILL"); } catch {}
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
sleepMs(300);
|
|
1042
|
-
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1043
|
-
return info;
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
function startHubAfterUpdate(info) {
|
|
1047
|
-
if (!info) return false;
|
|
1048
|
-
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
1049
|
-
if (!existsSync(serverPath)) return false;
|
|
1050
|
-
const port = Number(info?.port) > 0 ? String(info.port) : String(process.env.TFX_HUB_PORT || "27888");
|
|
1051
|
-
|
|
1052
|
-
try {
|
|
1053
|
-
const child = spawn(process.execPath, [serverPath], {
|
|
1054
|
-
env: { ...process.env, TFX_HUB_PORT: port },
|
|
1055
|
-
stdio: "ignore",
|
|
1056
|
-
detached: true,
|
|
1057
|
-
});
|
|
1058
|
-
child.unref();
|
|
1059
|
-
return true;
|
|
1060
|
-
} catch {
|
|
1061
|
-
return false;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1251
|
+
const HUB_PID_DIR = join(homedir(), ".claude", "cache", "tfx-hub");
|
|
1252
|
+
const HUB_PID_FILE = join(HUB_PID_DIR, "hub.pid");
|
|
1253
|
+
|
|
1254
|
+
function sleepMs(ms) {
|
|
1255
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function stopHubForUpdate() {
|
|
1259
|
+
if (!existsSync(HUB_PID_FILE)) return null;
|
|
1260
|
+
let info = null;
|
|
1261
|
+
try {
|
|
1262
|
+
info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1263
|
+
process.kill(info.pid, 0);
|
|
1264
|
+
} catch {
|
|
1265
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1266
|
+
return null;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
try {
|
|
1270
|
+
if (process.platform === "win32") {
|
|
1271
|
+
execSync(`taskkill /PID ${info.pid} /T /F`, {
|
|
1272
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1273
|
+
timeout: 10000,
|
|
1274
|
+
});
|
|
1275
|
+
} else {
|
|
1276
|
+
process.kill(info.pid, "SIGTERM");
|
|
1277
|
+
}
|
|
1278
|
+
} catch {
|
|
1279
|
+
try { process.kill(info.pid, "SIGKILL"); } catch {}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
sleepMs(300);
|
|
1283
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1284
|
+
return info;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
function startHubAfterUpdate(info) {
|
|
1288
|
+
if (!info) return false;
|
|
1289
|
+
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
1290
|
+
if (!existsSync(serverPath)) return false;
|
|
1291
|
+
const port = Number(info?.port) > 0 ? String(info.port) : String(process.env.TFX_HUB_PORT || "27888");
|
|
1292
|
+
|
|
1293
|
+
try {
|
|
1294
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
1295
|
+
env: { ...process.env, TFX_HUB_PORT: port },
|
|
1296
|
+
stdio: "ignore",
|
|
1297
|
+
detached: true,
|
|
1298
|
+
});
|
|
1299
|
+
child.unref();
|
|
1300
|
+
return true;
|
|
1301
|
+
} catch {
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1064
1305
|
|
|
1065
1306
|
// 설치된 CLI에 tfx-hub MCP 서버 자동 등록 (1회 설정, 이후 재실행 불필요)
|
|
1066
1307
|
function autoRegisterMcp(mcpUrl) {
|
|
@@ -1136,40 +1377,40 @@ function autoRegisterMcp(mcpUrl) {
|
|
|
1136
1377
|
} catch (e) { warn(`Claude 등록 실패: ${e.message}`); }
|
|
1137
1378
|
}
|
|
1138
1379
|
|
|
1139
|
-
async function cmdHub() {
|
|
1140
|
-
const sub = process.argv[3] || "status";
|
|
1141
|
-
const defaultPortRaw = Number(process.env.TFX_HUB_PORT || "27888");
|
|
1142
|
-
const probePort = Number.isFinite(defaultPortRaw) && defaultPortRaw > 0 ? defaultPortRaw : 27888;
|
|
1143
|
-
const formatHostForUrl = (host) => host.includes(":") ? `[${host}]` : host;
|
|
1144
|
-
const probeHubStatus = async (host = "127.0.0.1", port = probePort, timeoutMs = 3000) => {
|
|
1145
|
-
try {
|
|
1146
|
-
const res = await fetch(`http://${formatHostForUrl(host)}:${port}/status`, {
|
|
1147
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
1148
|
-
});
|
|
1149
|
-
if (!res.ok) return null;
|
|
1150
|
-
const data = await res.json();
|
|
1151
|
-
return data?.hub ? data : null;
|
|
1152
|
-
} catch {
|
|
1153
|
-
return null;
|
|
1154
|
-
}
|
|
1155
|
-
};
|
|
1156
|
-
const recoverPidFile = (statusData, defaultHost = "127.0.0.1") => {
|
|
1157
|
-
const pid = Number(statusData?.pid);
|
|
1158
|
-
const port = Number(statusData?.port) || probePort;
|
|
1159
|
-
if (!Number.isFinite(pid) || pid <= 0) return;
|
|
1160
|
-
try {
|
|
1161
|
-
mkdirSync(HUB_PID_DIR, { recursive: true });
|
|
1162
|
-
writeFileSync(HUB_PID_FILE, JSON.stringify({
|
|
1163
|
-
pid,
|
|
1164
|
-
port,
|
|
1165
|
-
host: defaultHost,
|
|
1166
|
-
url: `http://${formatHostForUrl(defaultHost)}:${port}/mcp`,
|
|
1167
|
-
started: Date.now(),
|
|
1168
|
-
}));
|
|
1169
|
-
} catch {}
|
|
1170
|
-
};
|
|
1171
|
-
|
|
1172
|
-
switch (sub) {
|
|
1380
|
+
async function cmdHub() {
|
|
1381
|
+
const sub = process.argv[3] || "status";
|
|
1382
|
+
const defaultPortRaw = Number(process.env.TFX_HUB_PORT || "27888");
|
|
1383
|
+
const probePort = Number.isFinite(defaultPortRaw) && defaultPortRaw > 0 ? defaultPortRaw : 27888;
|
|
1384
|
+
const formatHostForUrl = (host) => host.includes(":") ? `[${host}]` : host;
|
|
1385
|
+
const probeHubStatus = async (host = "127.0.0.1", port = probePort, timeoutMs = 3000) => {
|
|
1386
|
+
try {
|
|
1387
|
+
const res = await fetch(`http://${formatHostForUrl(host)}:${port}/status`, {
|
|
1388
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1389
|
+
});
|
|
1390
|
+
if (!res.ok) return null;
|
|
1391
|
+
const data = await res.json();
|
|
1392
|
+
return data?.hub ? data : null;
|
|
1393
|
+
} catch {
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
const recoverPidFile = (statusData, defaultHost = "127.0.0.1") => {
|
|
1398
|
+
const pid = Number(statusData?.pid);
|
|
1399
|
+
const port = Number(statusData?.port) || probePort;
|
|
1400
|
+
if (!Number.isFinite(pid) || pid <= 0) return;
|
|
1401
|
+
try {
|
|
1402
|
+
mkdirSync(HUB_PID_DIR, { recursive: true });
|
|
1403
|
+
writeFileSync(HUB_PID_FILE, JSON.stringify({
|
|
1404
|
+
pid,
|
|
1405
|
+
port,
|
|
1406
|
+
host: defaultHost,
|
|
1407
|
+
url: `http://${formatHostForUrl(defaultHost)}:${port}/mcp`,
|
|
1408
|
+
started: Date.now(),
|
|
1409
|
+
}));
|
|
1410
|
+
} catch {}
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1413
|
+
switch (sub) {
|
|
1173
1414
|
case "start": {
|
|
1174
1415
|
// 이미 실행 중인지 확인
|
|
1175
1416
|
if (existsSync(HUB_PID_FILE)) {
|
|
@@ -1200,13 +1441,13 @@ async function cmdHub() {
|
|
|
1200
1441
|
});
|
|
1201
1442
|
child.unref();
|
|
1202
1443
|
|
|
1203
|
-
// PID 파일 확인 (최대 3초 대기, 100ms 폴링)
|
|
1204
|
-
let started = false;
|
|
1205
|
-
const deadline = Date.now() + 3000;
|
|
1206
|
-
while (Date.now() < deadline) {
|
|
1207
|
-
if (existsSync(HUB_PID_FILE)) { started = true; break; }
|
|
1208
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
1209
|
-
}
|
|
1444
|
+
// PID 파일 확인 (최대 3초 대기, 100ms 폴링)
|
|
1445
|
+
let started = false;
|
|
1446
|
+
const deadline = Date.now() + 3000;
|
|
1447
|
+
while (Date.now() < deadline) {
|
|
1448
|
+
if (existsSync(HUB_PID_FILE)) { started = true; break; }
|
|
1449
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1450
|
+
}
|
|
1210
1451
|
|
|
1211
1452
|
if (started) {
|
|
1212
1453
|
const hubInfo = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
@@ -1225,20 +1466,20 @@ async function cmdHub() {
|
|
|
1225
1466
|
break;
|
|
1226
1467
|
}
|
|
1227
1468
|
|
|
1228
|
-
case "stop": {
|
|
1229
|
-
if (!existsSync(HUB_PID_FILE)) {
|
|
1230
|
-
const probed = await probeHubStatus("127.0.0.1", probePort, 1500)
|
|
1231
|
-
|| (probePort === 27888 ? null : await probeHubStatus("127.0.0.1", 27888, 1500));
|
|
1232
|
-
if (probed && Number.isFinite(Number(probed.pid))) {
|
|
1233
|
-
try {
|
|
1234
|
-
process.kill(Number(probed.pid), "SIGTERM");
|
|
1235
|
-
console.log(`\n ${GREEN_BRIGHT}✓${RESET} hub 종료됨 (PID ${probed.pid})${DIM} (probe)${RESET}\n`);
|
|
1236
|
-
return;
|
|
1237
|
-
} catch {}
|
|
1238
|
-
}
|
|
1239
|
-
console.log(`\n ${DIM}hub 미실행${RESET}\n`);
|
|
1240
|
-
return;
|
|
1241
|
-
}
|
|
1469
|
+
case "stop": {
|
|
1470
|
+
if (!existsSync(HUB_PID_FILE)) {
|
|
1471
|
+
const probed = await probeHubStatus("127.0.0.1", probePort, 1500)
|
|
1472
|
+
|| (probePort === 27888 ? null : await probeHubStatus("127.0.0.1", 27888, 1500));
|
|
1473
|
+
if (probed && Number.isFinite(Number(probed.pid))) {
|
|
1474
|
+
try {
|
|
1475
|
+
process.kill(Number(probed.pid), "SIGTERM");
|
|
1476
|
+
console.log(`\n ${GREEN_BRIGHT}✓${RESET} hub 종료됨 (PID ${probed.pid})${DIM} (probe)${RESET}\n`);
|
|
1477
|
+
return;
|
|
1478
|
+
} catch {}
|
|
1479
|
+
}
|
|
1480
|
+
console.log(`\n ${DIM}hub 미실행${RESET}\n`);
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1242
1483
|
try {
|
|
1243
1484
|
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1244
1485
|
process.kill(info.pid, "SIGTERM");
|
|
@@ -1250,36 +1491,36 @@ async function cmdHub() {
|
|
|
1250
1491
|
}
|
|
1251
1492
|
break;
|
|
1252
1493
|
}
|
|
1253
|
-
|
|
1254
|
-
case "status": {
|
|
1255
|
-
if (!existsSync(HUB_PID_FILE)) {
|
|
1256
|
-
const probed = await probeHubStatus();
|
|
1257
|
-
if (!probed) {
|
|
1258
|
-
const fallback = probePort === 27888 ? null : await probeHubStatus("127.0.0.1", 27888, 1500);
|
|
1259
|
-
if (fallback) {
|
|
1260
|
-
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(default port probe 성공)${RESET}`);
|
|
1261
|
-
console.log(` URL: http://127.0.0.1:${fallback.port || 27888}/mcp`);
|
|
1262
|
-
if (fallback.pid !== undefined) console.log(` PID: ${fallback.pid}`);
|
|
1263
|
-
if (fallback.hub?.state) console.log(` State: ${fallback.hub.state}`);
|
|
1264
|
-
if (fallback.sessions !== undefined) console.log(` Sessions: ${fallback.sessions}`);
|
|
1265
|
-
recoverPidFile(fallback, "127.0.0.1");
|
|
1266
|
-
console.log("");
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${RED}offline${RESET}\n`);
|
|
1270
|
-
return;
|
|
1271
|
-
}
|
|
1272
|
-
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(pid file 없음 / probe 성공)${RESET}`);
|
|
1273
|
-
console.log(` URL: http://127.0.0.1:${probed.port || probePort}/mcp`);
|
|
1274
|
-
if (probed.pid !== undefined) console.log(` PID: ${probed.pid}`);
|
|
1275
|
-
if (probed.hub?.state) console.log(` State: ${probed.hub.state}`);
|
|
1276
|
-
if (probed.sessions !== undefined) console.log(` Sessions: ${probed.sessions}`);
|
|
1277
|
-
recoverPidFile(probed, "127.0.0.1");
|
|
1278
|
-
console.log("");
|
|
1279
|
-
return;
|
|
1280
|
-
}
|
|
1281
|
-
try {
|
|
1282
|
-
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1494
|
+
|
|
1495
|
+
case "status": {
|
|
1496
|
+
if (!existsSync(HUB_PID_FILE)) {
|
|
1497
|
+
const probed = await probeHubStatus();
|
|
1498
|
+
if (!probed) {
|
|
1499
|
+
const fallback = probePort === 27888 ? null : await probeHubStatus("127.0.0.1", 27888, 1500);
|
|
1500
|
+
if (fallback) {
|
|
1501
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(default port probe 성공)${RESET}`);
|
|
1502
|
+
console.log(` URL: http://127.0.0.1:${fallback.port || 27888}/mcp`);
|
|
1503
|
+
if (fallback.pid !== undefined) console.log(` PID: ${fallback.pid}`);
|
|
1504
|
+
if (fallback.hub?.state) console.log(` State: ${fallback.hub.state}`);
|
|
1505
|
+
if (fallback.sessions !== undefined) console.log(` Sessions: ${fallback.sessions}`);
|
|
1506
|
+
recoverPidFile(fallback, "127.0.0.1");
|
|
1507
|
+
console.log("");
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${RED}offline${RESET}\n`);
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(pid file 없음 / probe 성공)${RESET}`);
|
|
1514
|
+
console.log(` URL: http://127.0.0.1:${probed.port || probePort}/mcp`);
|
|
1515
|
+
if (probed.pid !== undefined) console.log(` PID: ${probed.pid}`);
|
|
1516
|
+
if (probed.hub?.state) console.log(` State: ${probed.hub.state}`);
|
|
1517
|
+
if (probed.sessions !== undefined) console.log(` Sessions: ${probed.sessions}`);
|
|
1518
|
+
recoverPidFile(probed, "127.0.0.1");
|
|
1519
|
+
console.log("");
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
try {
|
|
1523
|
+
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1283
1524
|
process.kill(info.pid, 0); // 생존 확인
|
|
1284
1525
|
const uptime = Date.now() - info.started;
|
|
1285
1526
|
const uptimeStr = uptime < 60000 ? `${Math.round(uptime / 1000)}초`
|
|
@@ -1289,39 +1530,39 @@ async function cmdHub() {
|
|
|
1289
1530
|
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET}`);
|
|
1290
1531
|
console.log(` URL: ${info.url}`);
|
|
1291
1532
|
console.log(` PID: ${info.pid}`);
|
|
1292
|
-
console.log(` Uptime: ${uptimeStr}`);
|
|
1293
|
-
|
|
1294
|
-
// HTTP 상태 조회 시도
|
|
1295
|
-
try {
|
|
1296
|
-
const host = typeof info.host === "string" ? info.host : "127.0.0.1";
|
|
1297
|
-
const port = Number(info.port) || probePort;
|
|
1298
|
-
const data = await probeHubStatus(host, port, 3000);
|
|
1299
|
-
if (data.hub) {
|
|
1300
|
-
console.log(` State: ${data.hub.state}`);
|
|
1301
|
-
}
|
|
1533
|
+
console.log(` Uptime: ${uptimeStr}`);
|
|
1534
|
+
|
|
1535
|
+
// HTTP 상태 조회 시도
|
|
1536
|
+
try {
|
|
1537
|
+
const host = typeof info.host === "string" ? info.host : "127.0.0.1";
|
|
1538
|
+
const port = Number(info.port) || probePort;
|
|
1539
|
+
const data = await probeHubStatus(host, port, 3000);
|
|
1540
|
+
if (data.hub) {
|
|
1541
|
+
console.log(` State: ${data.hub.state}`);
|
|
1542
|
+
}
|
|
1302
1543
|
if (data.sessions !== undefined) {
|
|
1303
1544
|
console.log(` Sessions: ${data.sessions}`);
|
|
1304
1545
|
}
|
|
1305
1546
|
} catch {}
|
|
1306
1547
|
|
|
1307
1548
|
console.log("");
|
|
1308
|
-
} catch {
|
|
1309
|
-
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1310
|
-
const probed = await probeHubStatus();
|
|
1311
|
-
if (!probed) {
|
|
1312
|
-
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${RED}offline${RESET} ${DIM}(stale PID 정리됨)${RESET}\n`);
|
|
1313
|
-
break;
|
|
1314
|
-
}
|
|
1315
|
-
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(stale PID 정리 후 probe 성공)${RESET}`);
|
|
1316
|
-
console.log(` URL: http://127.0.0.1:${probed.port || probePort}/mcp`);
|
|
1317
|
-
if (probed.pid !== undefined) console.log(` PID: ${probed.pid}`);
|
|
1318
|
-
if (probed.hub?.state) console.log(` State: ${probed.hub.state}`);
|
|
1319
|
-
if (probed.sessions !== undefined) console.log(` Sessions: ${probed.sessions}`);
|
|
1320
|
-
recoverPidFile(probed, "127.0.0.1");
|
|
1321
|
-
console.log("");
|
|
1322
|
-
}
|
|
1323
|
-
break;
|
|
1324
|
-
}
|
|
1549
|
+
} catch {
|
|
1550
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1551
|
+
const probed = await probeHubStatus();
|
|
1552
|
+
if (!probed) {
|
|
1553
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${RED}offline${RESET} ${DIM}(stale PID 정리됨)${RESET}\n`);
|
|
1554
|
+
break;
|
|
1555
|
+
}
|
|
1556
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET} ${DIM}(stale PID 정리 후 probe 성공)${RESET}`);
|
|
1557
|
+
console.log(` URL: http://127.0.0.1:${probed.port || probePort}/mcp`);
|
|
1558
|
+
if (probed.pid !== undefined) console.log(` PID: ${probed.pid}`);
|
|
1559
|
+
if (probed.hub?.state) console.log(` State: ${probed.hub.state}`);
|
|
1560
|
+
if (probed.sessions !== undefined) console.log(` Sessions: ${probed.sessions}`);
|
|
1561
|
+
recoverPidFile(probed, "127.0.0.1");
|
|
1562
|
+
console.log("");
|
|
1563
|
+
}
|
|
1564
|
+
break;
|
|
1565
|
+
}
|
|
1325
1566
|
|
|
1326
1567
|
default:
|
|
1327
1568
|
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET}\n`);
|
|
@@ -1338,15 +1579,15 @@ const cmd = process.argv[2] || "help";
|
|
|
1338
1579
|
|
|
1339
1580
|
switch (cmd) {
|
|
1340
1581
|
case "setup": cmdSetup(); break;
|
|
1341
|
-
case "doctor": {
|
|
1342
|
-
const fix = process.argv.includes("--fix");
|
|
1343
|
-
const reset = process.argv.includes("--reset");
|
|
1344
|
-
cmdDoctor({ fix, reset });
|
|
1345
|
-
break;
|
|
1346
|
-
}
|
|
1582
|
+
case "doctor": {
|
|
1583
|
+
const fix = process.argv.includes("--fix");
|
|
1584
|
+
const reset = process.argv.includes("--reset");
|
|
1585
|
+
await cmdDoctor({ fix, reset });
|
|
1586
|
+
break;
|
|
1587
|
+
}
|
|
1347
1588
|
case "update": cmdUpdate(); break;
|
|
1348
1589
|
case "list": case "ls": cmdList(); break;
|
|
1349
|
-
case "hub": await cmdHub(); break;
|
|
1590
|
+
case "hub": await cmdHub(); break;
|
|
1350
1591
|
case "team": {
|
|
1351
1592
|
const { pathToFileURL } = await import("node:url");
|
|
1352
1593
|
const { cmdTeam } = await import(pathToFileURL(join(PKG_ROOT, "hub", "team", "cli.mjs")).href);
|