triflux 3.2.0-dev.8 → 3.3.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/triflux.mjs +1296 -1055
- package/hooks/hooks.json +17 -0
- package/hooks/keyword-rules.json +20 -4
- package/hooks/pipeline-stop.mjs +54 -0
- package/hub/bridge.mjs +517 -318
- package/hub/hitl.mjs +45 -31
- package/hub/pipe.mjs +457 -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 +422 -161
- package/hub/schema.sql +14 -0
- package/hub/server.mjs +499 -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 +516 -0
- package/hub/team/cli-team-status.mjs +269 -0
- package/hub/team/cli.mjs +75 -1475
- package/hub/team/dashboard.mjs +1 -9
- package/hub/team/native.mjs +190 -130
- package/hub/team/nativeProxy.mjs +165 -78
- package/hub/team/orchestrator.mjs +15 -20
- package/hub/team/pane.mjs +137 -103
- package/hub/team/psmux.mjs +506 -0
- package/hub/team/session.mjs +393 -330
- package/hub/team/shared.mjs +13 -0
- package/hub/team/staleState.mjs +299 -0
- package/hub/tools.mjs +105 -31
- 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 +1790 -1788
- package/package.json +4 -1
- package/scripts/__tests__/keyword-detector.test.mjs +8 -8
- package/scripts/keyword-detector.mjs +15 -0
- package/scripts/lib/keyword-rules.mjs +4 -1
- package/scripts/preflight-cache.mjs +72 -0
- package/scripts/psmux-steering-prototype.sh +368 -0
- package/scripts/setup.mjs +136 -71
- package/scripts/tfx-route-worker.mjs +161 -0
- package/scripts/tfx-route.sh +485 -91
- 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-multi/SKILL.md +378 -0
- package/skills/tfx-setup/SKILL.md +1 -4
- package/skills/tfx-team/SKILL.md +0 -304
package/bin/triflux.mjs
CHANGED
|
@@ -1,448 +1,575 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
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";
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// triflux CLI — setup, doctor, version
|
|
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
|
+
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";
|
|
8
11
|
|
|
9
12
|
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
10
|
-
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
11
|
-
const CODEX_DIR = join(homedir(), ".codex");
|
|
12
|
-
const CODEX_CONFIG_PATH = join(CODEX_DIR, "config.toml");
|
|
13
|
-
const PKG = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
14
|
-
|
|
15
|
-
const REQUIRED_CODEX_PROFILES = [
|
|
16
|
-
{
|
|
17
|
-
name: "xhigh",
|
|
18
|
-
lines: [
|
|
19
|
-
'model = "gpt-5.3-codex"',
|
|
20
|
-
'model_reasoning_effort = "xhigh"',
|
|
21
|
-
],
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
name: "spark_fast",
|
|
25
|
-
lines: [
|
|
26
|
-
'model = "gpt-5.1-codex-mini"',
|
|
27
|
-
'model_reasoning_effort = "low"',
|
|
28
|
-
],
|
|
29
|
-
},
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
// ── 색상 체계 (triflux brand: amber/orange accent) ──
|
|
33
|
-
const CYAN = "\x1b[36m";
|
|
34
|
-
const GREEN = "\x1b[32m";
|
|
35
|
-
const RED = "\x1b[31m";
|
|
36
|
-
const YELLOW = "\x1b[33m";
|
|
37
|
-
const DIM = "\x1b[2m";
|
|
38
|
-
const BOLD = "\x1b[1m";
|
|
39
|
-
const RESET = "\x1b[0m";
|
|
40
|
-
const AMBER = "\x1b[38;5;214m";
|
|
41
|
-
const BLUE = "\x1b[38;5;39m";
|
|
42
|
-
const WHITE_BRIGHT = "\x1b[97m";
|
|
43
|
-
const GRAY = "\x1b[38;5;245m";
|
|
44
|
-
const GREEN_BRIGHT = "\x1b[38;5;82m";
|
|
45
|
-
const RED_BRIGHT = "\x1b[38;5;196m";
|
|
46
|
-
|
|
47
|
-
// ── 브랜드 요소 ──
|
|
48
|
-
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
function
|
|
57
|
-
function
|
|
58
|
-
function
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
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
|
+
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;
|
|
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 = execSync(
|
|
68
|
+
process.platform === "win32" ? `where ${cmd} 2>nul` : `which ${cmd} 2>/dev/null`,
|
|
69
|
+
{ encoding: "utf8", timeout: 5000 }
|
|
70
|
+
).trim();
|
|
71
|
+
return result.split(/\r?\n/)[0] || null;
|
|
72
|
+
} catch { return null; }
|
|
73
|
+
}
|
|
74
|
+
|
|
71
75
|
function whichInShell(cmd, shell) {
|
|
72
|
-
const cmds = {
|
|
73
|
-
bash: `bash -c "source ~/.bashrc 2>/dev/null && command -v ${cmd} 2>/dev/null"`,
|
|
74
|
-
cmd: `cmd /c where ${cmd} 2>nul`,
|
|
75
|
-
pwsh: `pwsh -NoProfile -c "(Get-Command ${cmd} -EA SilentlyContinue).Source"`,
|
|
76
|
-
};
|
|
77
|
-
const command = cmds[shell];
|
|
78
|
-
if (!command) return null;
|
|
79
|
-
try {
|
|
80
|
-
const result = execSync(command, {
|
|
81
|
-
encoding: "utf8",
|
|
82
|
-
timeout: 8000,
|
|
83
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
84
|
-
}).trim();
|
|
85
|
-
return result.split(/\r?\n/)[0] || null;
|
|
76
|
+
const cmds = {
|
|
77
|
+
bash: `bash -c "source ~/.bashrc 2>/dev/null && command -v ${cmd} 2>/dev/null"`,
|
|
78
|
+
cmd: `cmd /c where ${cmd} 2>nul`,
|
|
79
|
+
pwsh: `pwsh -NoProfile -c "(Get-Command ${cmd} -EA SilentlyContinue).Source"`,
|
|
80
|
+
};
|
|
81
|
+
const command = cmds[shell];
|
|
82
|
+
if (!command) return null;
|
|
83
|
+
try {
|
|
84
|
+
const result = execSync(command, {
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
timeout: 8000,
|
|
87
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
88
|
+
}).trim();
|
|
89
|
+
return result.split(/\r?\n/)[0] || null;
|
|
86
90
|
} catch { return null; }
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
function isDevUpdateRequested(argv = process.argv) {
|
|
90
94
|
return argv.includes("--dev") || argv.includes("@dev") || argv.includes("dev");
|
|
91
95
|
}
|
|
92
|
-
|
|
93
|
-
function checkShellAvailable(shell) {
|
|
94
|
-
const cmds = { bash: "bash --version", cmd: "cmd /c echo ok", pwsh: "pwsh -NoProfile -c echo ok" };
|
|
95
|
-
try {
|
|
96
|
-
execSync(cmds[shell], { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "ignore"] });
|
|
97
|
-
return true;
|
|
98
|
-
} catch { return false; }
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function getVersion(filePath) {
|
|
102
|
-
try {
|
|
103
|
-
const content = readFileSync(filePath, "utf8");
|
|
104
|
-
const match = content.match(/VERSION\s*=\s*"([^"]+)"/);
|
|
105
|
-
return match ? match[1] : null;
|
|
106
|
-
} catch { return null; }
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
96
|
+
|
|
97
|
+
function checkShellAvailable(shell) {
|
|
98
|
+
const cmds = { bash: "bash --version", cmd: "cmd /c echo ok", pwsh: "pwsh -NoProfile -c echo ok" };
|
|
99
|
+
try {
|
|
100
|
+
execSync(cmds[shell], { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "ignore"] });
|
|
101
|
+
return true;
|
|
102
|
+
} catch { return false; }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getVersion(filePath) {
|
|
106
|
+
try {
|
|
107
|
+
const content = readFileSync(filePath, "utf8");
|
|
108
|
+
const match = content.match(/VERSION\s*=\s*"([^"]+)"/);
|
|
109
|
+
return match ? match[1] : null;
|
|
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
|
+
}
|
|
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
|
+
|
|
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`);
|
|
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
|
+
execSync(`"${process.execPath}" "${mcpCheck}"`, { timeout: 15000, stdio: "ignore" });
|
|
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
|
+
execSync(`"${process.execPath}" "${hudScript}" --refresh-claude-usage`, { timeout: 20000, stdio: "ignore" });
|
|
524
|
+
ok("Claude 사용량 캐시 재생성됨");
|
|
525
|
+
} catch { warn("Claude 사용량 캐시 재생성 실패 — 다음 API 호출 시 자동 생성"); }
|
|
526
|
+
try {
|
|
527
|
+
execSync(`"${process.execPath}" "${hudScript}" --refresh-codex-rate-limits`, { timeout: 15000, stdio: "ignore" });
|
|
528
|
+
ok("Codex 레이트 리밋 캐시 재생성됨");
|
|
529
|
+
} catch { warn("Codex 레이트 리밋 캐시 재생성 실패"); }
|
|
530
|
+
try {
|
|
531
|
+
execSync(`"${process.execPath}" "${hudScript}" --refresh-gemini-quota`, { timeout: 15000, stdio: "ignore" });
|
|
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");
|
|
434
561
|
if (existsSync(fSkillsSrc)) {
|
|
435
562
|
let sc = 0, st = 0;
|
|
436
563
|
for (const name of readdirSync(fSkillsSrc)) {
|
|
437
564
|
const src = join(fSkillsSrc, name, "SKILL.md");
|
|
438
|
-
const dst = join(fSkillsDst, name, "SKILL.md");
|
|
439
|
-
if (!existsSync(src)) continue;
|
|
440
|
-
st++;
|
|
441
|
-
const dstDir = dirname(dst);
|
|
442
|
-
if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true });
|
|
443
|
-
if (!existsSync(dst)) { copyFileSync(src, dst); sc++; }
|
|
444
|
-
else if (readFileSync(src, "utf8") !== readFileSync(dst, "utf8")) { copyFileSync(src, dst); sc++; }
|
|
445
|
-
}
|
|
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
|
+
}
|
|
446
573
|
if (sc > 0) ok(`스킬: ${sc}/${st}개 업데이트됨`);
|
|
447
574
|
else ok(`스킬: ${st}개 최신 상태`);
|
|
448
575
|
}
|
|
@@ -458,45 +585,45 @@ function cmdDoctor(options = {}) {
|
|
|
458
585
|
const fCacheDir = join(CLAUDE_DIR, "cache");
|
|
459
586
|
const staleNames = ["claude-usage-cache.json", ".claude-refresh-lock", "codex-rate-limits-cache.json"];
|
|
460
587
|
let cleaned = 0;
|
|
461
|
-
for (const name of staleNames) {
|
|
462
|
-
const fp = join(fCacheDir, name);
|
|
463
|
-
if (!existsSync(fp)) continue;
|
|
464
|
-
try {
|
|
465
|
-
const parsed = JSON.parse(readFileSync(fp, "utf8"));
|
|
466
|
-
if (parsed.error || name.startsWith(".")) { unlinkSync(fp); cleaned++; ok(`에러 캐시 정리: ${name}`); }
|
|
467
|
-
} catch { try { unlinkSync(fp); cleaned++; ok(`손상된 캐시 정리: ${name}`); } catch {} }
|
|
468
|
-
}
|
|
469
|
-
if (cleaned === 0) info("에러 캐시 없음");
|
|
470
|
-
console.log(`\n ${LINE}`);
|
|
471
|
-
info("수정 완료 — 아래 진단 결과를 확인하세요");
|
|
472
|
-
console.log("");
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
let issues = 0;
|
|
476
|
-
|
|
477
|
-
// 1. tfx-route.sh
|
|
478
|
-
section("tfx-route.sh");
|
|
479
|
-
const routeSh = join(CLAUDE_DIR, "scripts", "tfx-route.sh");
|
|
480
|
-
if (existsSync(routeSh)) {
|
|
481
|
-
const ver = getVersion(routeSh);
|
|
482
|
-
ok(`설치됨 ${ver ? `${DIM}v${ver}${RESET}` : ""}`);
|
|
483
|
-
} else {
|
|
484
|
-
fail("미설치 — tfx setup 실행 필요");
|
|
485
|
-
issues++;
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// 2. HUD
|
|
489
|
-
section("HUD");
|
|
490
|
-
const hud = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
491
|
-
if (existsSync(hud)) {
|
|
492
|
-
ok("설치됨");
|
|
493
|
-
} else {
|
|
494
|
-
warn("미설치 ${GRAY}(선택사항)${RESET}");
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// 3. Codex CLI
|
|
498
|
-
section(`Codex CLI ${WHITE_BRIGHT}●${RESET}`);
|
|
499
|
-
issues += checkCliCrossShell("codex", "npm install -g @openai/codex");
|
|
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");
|
|
500
627
|
if (which("codex")) {
|
|
501
628
|
if (process.env.OPENAI_API_KEY) {
|
|
502
629
|
ok("OPENAI_API_KEY 설정됨");
|
|
@@ -528,214 +655,328 @@ function cmdDoctor(options = {}) {
|
|
|
528
655
|
if (which("gemini")) {
|
|
529
656
|
if (process.env.GEMINI_API_KEY) {
|
|
530
657
|
ok("GEMINI_API_KEY 설정됨");
|
|
531
|
-
} else {
|
|
532
|
-
warn(`GEMINI_API_KEY 미설정 ${GRAY}(gemini auth login)${RESET}`);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
658
|
+
} else {
|
|
659
|
+
warn(`GEMINI_API_KEY 미설정 ${GRAY}(gemini auth login)${RESET}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
536
663
|
// 6. Claude Code
|
|
537
664
|
section(`Claude Code ${AMBER}●${RESET}`);
|
|
538
665
|
const claudePath = which("claude");
|
|
539
666
|
if (claudePath) {
|
|
540
667
|
ok("설치됨");
|
|
541
668
|
} else {
|
|
542
|
-
fail("미설치 (필수)");
|
|
543
|
-
issues++;
|
|
544
|
-
}
|
|
545
|
-
|
|
669
|
+
fail("미설치 (필수)");
|
|
670
|
+
issues++;
|
|
671
|
+
}
|
|
672
|
+
|
|
546
673
|
// 7. 스킬 설치 상태
|
|
547
674
|
section("Skills");
|
|
548
675
|
const skillsSrc = join(PKG_ROOT, "skills");
|
|
549
676
|
const skillsDst = join(CLAUDE_DIR, "skills");
|
|
550
|
-
if (existsSync(skillsSrc)) {
|
|
551
|
-
let installed = 0;
|
|
552
|
-
let total = 0;
|
|
553
|
-
const missing = [];
|
|
554
|
-
for (const name of readdirSync(skillsSrc)) {
|
|
555
|
-
if (!existsSync(join(skillsSrc, name, "SKILL.md"))) continue;
|
|
556
|
-
total++;
|
|
557
|
-
if (existsSync(join(skillsDst, name, "SKILL.md"))) {
|
|
558
|
-
installed++;
|
|
559
|
-
} else {
|
|
560
|
-
missing.push(name);
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
if (installed === total) {
|
|
564
|
-
ok(`${installed}/${total}개 설치됨`);
|
|
565
|
-
} else {
|
|
566
|
-
warn(`${installed}/${total}개 설치됨 — 미설치: ${missing.join(", ")}`);
|
|
567
|
-
info("triflux setup으로 동기화 가능");
|
|
568
|
-
issues++;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
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
|
+
|
|
572
699
|
// 8. 플러그인 등록
|
|
573
700
|
section("Plugin");
|
|
574
701
|
const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
|
|
575
|
-
if (existsSync(pluginsFile)) {
|
|
576
|
-
const content = readFileSync(pluginsFile, "utf8");
|
|
577
|
-
if (content.includes("triflux")) {
|
|
578
|
-
ok("triflux 플러그인 등록됨");
|
|
579
|
-
} else {
|
|
580
|
-
warn("triflux 플러그인 미등록 — npm 단독 사용 중");
|
|
581
|
-
info("플러그인 등록: /plugin marketplace add <repo-url>");
|
|
582
|
-
}
|
|
583
|
-
} else {
|
|
584
|
-
info("플러그인 시스템 감지 안 됨 — npm 단독 사용");
|
|
585
|
-
}
|
|
586
|
-
|
|
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
|
+
|
|
587
714
|
// 9. MCP 인벤토리
|
|
588
715
|
section("MCP Inventory");
|
|
589
716
|
const mcpCache = join(CLAUDE_DIR, "cache", "mcp-inventory.json");
|
|
590
|
-
if (existsSync(mcpCache)) {
|
|
591
|
-
try {
|
|
592
|
-
const inv = JSON.parse(readFileSync(mcpCache, "utf8"));
|
|
593
|
-
ok(`캐시 존재 (${inv.timestamp})`);
|
|
594
|
-
if (inv.codex?.servers?.length) {
|
|
595
|
-
const names = inv.codex.servers.map(s => s.name).join(", ");
|
|
596
|
-
info(`Codex: ${inv.codex.servers.length}개 서버 (${names})`);
|
|
597
|
-
}
|
|
598
|
-
if (inv.gemini?.servers?.length) {
|
|
599
|
-
const names = inv.gemini.servers.map(s => s.name).join(", ");
|
|
600
|
-
info(`Gemini: ${inv.gemini.servers.length}개 서버 (${names})`);
|
|
601
|
-
}
|
|
602
|
-
} catch {
|
|
603
|
-
warn("캐시 파일 파싱 실패");
|
|
604
|
-
}
|
|
605
|
-
} else {
|
|
606
|
-
warn("캐시 없음 — 다음 세션 시작 시 자동 생성");
|
|
607
|
-
info(`수동: node ${join(PKG_ROOT, "scripts", "mcp-check.mjs")}`);
|
|
608
|
-
}
|
|
609
|
-
|
|
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
|
+
|
|
610
737
|
// 10. CLI 이슈 트래커
|
|
611
738
|
section("CLI Issues");
|
|
612
|
-
const issuesFile = join(CLAUDE_DIR, "cache", "cli-issues.jsonl");
|
|
613
|
-
if (existsSync(issuesFile)) {
|
|
614
|
-
try {
|
|
615
|
-
const lines = readFileSync(issuesFile, "utf8").trim().split("\n").filter(Boolean);
|
|
616
|
-
const entries = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
617
|
-
const unresolved = entries.filter(e => !e.resolved);
|
|
618
|
-
|
|
619
|
-
if (unresolved.length === 0) {
|
|
620
|
-
ok("미해결 이슈 없음");
|
|
621
|
-
} else {
|
|
622
|
-
// 패턴별 그룹핑
|
|
623
|
-
const groups = {};
|
|
624
|
-
for (const e of unresolved) {
|
|
625
|
-
const key = `${e.cli}:${e.pattern}`;
|
|
626
|
-
if (!groups[key]) groups[key] = { ...e, count: 0 };
|
|
627
|
-
groups[key].count++;
|
|
628
|
-
if (e.ts > groups[key].ts) { groups[key].ts = e.ts; groups[key].snippet = e.snippet; }
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// 알려진 해결 버전 (패턴별 수정된 triflux 버전)
|
|
632
|
-
const KNOWN_FIXES = {
|
|
633
|
-
"gemini:deprecated_flag": "1.8.9", // -p → --prompt
|
|
634
|
-
};
|
|
635
|
-
|
|
636
|
-
const currentVer = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8")).version;
|
|
637
|
-
let cleaned = 0;
|
|
638
|
-
|
|
639
|
-
for (const [key, g] of Object.entries(groups)) {
|
|
640
|
-
const fixVer = KNOWN_FIXES[key];
|
|
641
|
-
if (fixVer && currentVer >= fixVer) {
|
|
642
|
-
// 해결된 이슈 — 자동 정리
|
|
643
|
-
cleaned += g.count;
|
|
644
|
-
continue;
|
|
645
|
-
}
|
|
646
|
-
const age = Date.now() - g.ts;
|
|
647
|
-
const ago = age < 3600000 ? `${Math.round(age / 60000)}분 전` :
|
|
648
|
-
age < 86400000 ? `${Math.round(age / 3600000)}시간 전` :
|
|
649
|
-
`${Math.round(age / 86400000)}일 전`;
|
|
650
|
-
const sev = g.severity === "error" ? `${RED}ERROR${RESET}` : `${YELLOW}WARN${RESET}`;
|
|
651
|
-
warn(`[${sev}] ${g.cli}/${g.pattern} x${g.count} (최근: ${ago})`);
|
|
652
|
-
if (g.snippet) info(` ${g.snippet.substring(0, 120)}`);
|
|
653
|
-
if (fixVer) info(` 해결: triflux >= v${fixVer} (npm update -g triflux)`);
|
|
654
|
-
issues++;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// 해결된 이슈 자동 정리
|
|
658
|
-
if (cleaned > 0) {
|
|
659
|
-
const remaining = entries.filter(e => {
|
|
660
|
-
const key = `${e.cli}:${e.pattern}`;
|
|
661
|
-
const fixVer = KNOWN_FIXES[key];
|
|
662
|
-
return !(fixVer && currentVer >= fixVer);
|
|
663
|
-
});
|
|
664
|
-
writeFileSync(issuesFile, remaining.map(e => JSON.stringify(e)).join("\n") + (remaining.length ? "\n" : ""));
|
|
665
|
-
ok(`${cleaned}개 해결된 이슈 자동 정리됨`);
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
} catch (e) {
|
|
669
|
-
warn(`이슈 파일 읽기 실패: ${e.message}`);
|
|
670
|
-
}
|
|
671
|
-
} else {
|
|
672
|
-
ok("이슈 로그 없음 (정상)");
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
//
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
}
|
|
683
|
-
|
|
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
|
+
|
|
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
|
+
|
|
916
|
+
// 결과
|
|
917
|
+
console.log(`\n ${LINE}`);
|
|
918
|
+
if (issues === 0) {
|
|
919
|
+
console.log(` ${GREEN_BRIGHT}${BOLD}✓ 모든 검사 통과${RESET}\n`);
|
|
920
|
+
} else {
|
|
921
|
+
console.log(` ${YELLOW}${BOLD}⚠ ${issues}개 항목 확인 필요${RESET}\n`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
684
925
|
function cmdUpdate() {
|
|
685
926
|
const isDev = isDevUpdateRequested(process.argv);
|
|
686
927
|
const tagLabel = isDev ? ` ${YELLOW}--dev${RESET}` : "";
|
|
687
928
|
console.log(`\n${BOLD}triflux update${RESET}${tagLabel}\n`);
|
|
688
|
-
|
|
689
|
-
// 1. 설치 방식 감지
|
|
690
|
-
const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
|
|
691
|
-
let installMode = "unknown";
|
|
692
|
-
let pluginPath = null;
|
|
693
|
-
|
|
694
|
-
// 플러그인 모드 감지
|
|
695
|
-
if (existsSync(pluginsFile)) {
|
|
696
|
-
try {
|
|
697
|
-
const plugins = JSON.parse(readFileSync(pluginsFile, "utf8"));
|
|
698
|
-
for (const [key, entries] of Object.entries(plugins.plugins || {})) {
|
|
699
|
-
if (key.startsWith("triflux")) {
|
|
700
|
-
pluginPath = entries[0]?.installPath;
|
|
701
|
-
installMode = "plugin";
|
|
702
|
-
break;
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
} catch {}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
// PKG_ROOT가 플러그인 캐시 내에 있으면 플러그인 모드
|
|
709
|
-
if (installMode === "unknown" && PKG_ROOT.includes(join(".claude", "plugins"))) {
|
|
710
|
-
installMode = "plugin";
|
|
711
|
-
pluginPath = PKG_ROOT;
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// npm global 감지
|
|
715
|
-
if (installMode === "unknown") {
|
|
716
|
-
try {
|
|
717
|
-
const npmList = execSync("npm list -g triflux --depth=0", {
|
|
718
|
-
encoding: "utf8",
|
|
719
|
-
timeout: 10000,
|
|
720
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
721
|
-
});
|
|
722
|
-
if (npmList.includes("triflux")) installMode = "npm-global";
|
|
723
|
-
} catch {}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
// npm local 감지
|
|
727
|
-
if (installMode === "unknown") {
|
|
728
|
-
const localPkg = join(process.cwd(), "node_modules", "triflux");
|
|
729
|
-
if (existsSync(localPkg)) installMode = "npm-local";
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// git 저장소 직접 사용
|
|
733
|
-
if (installMode === "unknown" && existsSync(join(PKG_ROOT, ".git"))) {
|
|
734
|
-
installMode = "git-local";
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
info(`검색: ${installMode === "plugin" ? "플러그인" : installMode === "npm-global" ? "npm global" : installMode === "npm-local" ? "npm local" : installMode === "git-local" ? "git 로컬 저장소" : "알 수 없음"} 설치 감지`);
|
|
738
|
-
|
|
929
|
+
|
|
930
|
+
// 1. 설치 방식 감지
|
|
931
|
+
const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
|
|
932
|
+
let installMode = "unknown";
|
|
933
|
+
let pluginPath = null;
|
|
934
|
+
|
|
935
|
+
// 플러그인 모드 감지
|
|
936
|
+
if (existsSync(pluginsFile)) {
|
|
937
|
+
try {
|
|
938
|
+
const plugins = JSON.parse(readFileSync(pluginsFile, "utf8"));
|
|
939
|
+
for (const [key, entries] of Object.entries(plugins.plugins || {})) {
|
|
940
|
+
if (key.startsWith("triflux")) {
|
|
941
|
+
pluginPath = entries[0]?.installPath;
|
|
942
|
+
installMode = "plugin";
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
} catch {}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// PKG_ROOT가 플러그인 캐시 내에 있으면 플러그인 모드
|
|
950
|
+
if (installMode === "unknown" && PKG_ROOT.includes(join(".claude", "plugins"))) {
|
|
951
|
+
installMode = "plugin";
|
|
952
|
+
pluginPath = PKG_ROOT;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// npm global 감지
|
|
956
|
+
if (installMode === "unknown") {
|
|
957
|
+
try {
|
|
958
|
+
const npmList = execSync("npm list -g triflux --depth=0", {
|
|
959
|
+
encoding: "utf8",
|
|
960
|
+
timeout: 10000,
|
|
961
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
962
|
+
});
|
|
963
|
+
if (npmList.includes("triflux")) installMode = "npm-global";
|
|
964
|
+
} catch {}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// npm local 감지
|
|
968
|
+
if (installMode === "unknown") {
|
|
969
|
+
const localPkg = join(process.cwd(), "node_modules", "triflux");
|
|
970
|
+
if (existsSync(localPkg)) installMode = "npm-local";
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// git 저장소 직접 사용
|
|
974
|
+
if (installMode === "unknown" && existsSync(join(PKG_ROOT, ".git"))) {
|
|
975
|
+
installMode = "git-local";
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
info(`검색: ${installMode === "plugin" ? "플러그인" : installMode === "npm-global" ? "npm global" : installMode === "npm-local" ? "npm local" : installMode === "git-local" ? "git 로컬 저장소" : "알 수 없음"} 설치 감지`);
|
|
979
|
+
|
|
739
980
|
// 2. 설치 방식에 따라 업데이트
|
|
740
981
|
const oldVer = PKG.version;
|
|
741
982
|
let updated = false;
|
|
@@ -743,16 +984,16 @@ function cmdUpdate() {
|
|
|
743
984
|
|
|
744
985
|
try {
|
|
745
986
|
switch (installMode) {
|
|
746
|
-
case "plugin": {
|
|
747
|
-
const gitDir = pluginPath || PKG_ROOT;
|
|
748
|
-
const result = execSync("git pull", {
|
|
749
|
-
encoding: "utf8",
|
|
750
|
-
timeout: 30000,
|
|
751
|
-
cwd: gitDir,
|
|
752
|
-
}).trim();
|
|
753
|
-
ok(`git pull — ${result}`);
|
|
754
|
-
updated = true;
|
|
755
|
-
break;
|
|
987
|
+
case "plugin": {
|
|
988
|
+
const gitDir = pluginPath || PKG_ROOT;
|
|
989
|
+
const result = execSync("git pull", {
|
|
990
|
+
encoding: "utf8",
|
|
991
|
+
timeout: 30000,
|
|
992
|
+
cwd: gitDir,
|
|
993
|
+
}).trim();
|
|
994
|
+
ok(`git pull — ${result}`);
|
|
995
|
+
updated = true;
|
|
996
|
+
break;
|
|
756
997
|
}
|
|
757
998
|
case "npm-global": {
|
|
758
999
|
stoppedHubInfo = stopHubForUpdate();
|
|
@@ -762,39 +1003,39 @@ function cmdUpdate() {
|
|
|
762
1003
|
const npmCmd = isDev ? "npm install -g triflux@dev" : "npm update -g triflux";
|
|
763
1004
|
const result = execSync(npmCmd, {
|
|
764
1005
|
encoding: "utf8",
|
|
765
|
-
timeout: 60000,
|
|
766
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
767
|
-
}).trim().split(/\r?\n/)[0];
|
|
1006
|
+
timeout: 60000,
|
|
1007
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1008
|
+
}).trim().split(/\r?\n/)[0];
|
|
768
1009
|
ok(`${isDev ? "npm install -g triflux@dev" : "npm update -g triflux"} — ${result || "완료"}`);
|
|
769
|
-
updated = true;
|
|
770
|
-
break;
|
|
771
|
-
}
|
|
772
|
-
case "npm-local": {
|
|
773
|
-
const npmLocalCmd = isDev ? "npm install triflux@dev" : "npm update triflux";
|
|
774
|
-
const result = execSync(npmLocalCmd, {
|
|
775
|
-
encoding: "utf8",
|
|
776
|
-
timeout: 60000,
|
|
777
|
-
cwd: process.cwd(),
|
|
778
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
779
|
-
}).trim().split(/\r?\n/)[0];
|
|
1010
|
+
updated = true;
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
case "npm-local": {
|
|
1014
|
+
const npmLocalCmd = isDev ? "npm install triflux@dev" : "npm update triflux";
|
|
1015
|
+
const result = execSync(npmLocalCmd, {
|
|
1016
|
+
encoding: "utf8",
|
|
1017
|
+
timeout: 60000,
|
|
1018
|
+
cwd: process.cwd(),
|
|
1019
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1020
|
+
}).trim().split(/\r?\n/)[0];
|
|
780
1021
|
ok(`${isDev ? "npm install triflux@dev" : "npm update triflux"} — ${result || "완료"}`);
|
|
781
|
-
updated = true;
|
|
782
|
-
break;
|
|
783
|
-
}
|
|
784
|
-
case "git-local": {
|
|
785
|
-
const result = execSync("git pull", {
|
|
786
|
-
encoding: "utf8",
|
|
787
|
-
timeout: 30000,
|
|
788
|
-
cwd: PKG_ROOT,
|
|
789
|
-
}).trim();
|
|
790
|
-
ok(`git pull — ${result}`);
|
|
791
|
-
updated = true;
|
|
792
|
-
break;
|
|
793
|
-
}
|
|
794
|
-
default:
|
|
795
|
-
fail("설치 방식을 감지할 수 없음");
|
|
796
|
-
info("수동 업데이트: cd <triflux-dir> && git pull");
|
|
797
|
-
return;
|
|
1022
|
+
updated = true;
|
|
1023
|
+
break;
|
|
1024
|
+
}
|
|
1025
|
+
case "git-local": {
|
|
1026
|
+
const result = execSync("git pull", {
|
|
1027
|
+
encoding: "utf8",
|
|
1028
|
+
timeout: 30000,
|
|
1029
|
+
cwd: PKG_ROOT,
|
|
1030
|
+
}).trim();
|
|
1031
|
+
ok(`git pull — ${result}`);
|
|
1032
|
+
updated = true;
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
default:
|
|
1036
|
+
fail("설치 방식을 감지할 수 없음");
|
|
1037
|
+
info("수동 업데이트: cd <triflux-dir> && git pull");
|
|
1038
|
+
return;
|
|
798
1039
|
}
|
|
799
1040
|
} catch (e) {
|
|
800
1041
|
if (stoppedHubInfo && startHubAfterUpdate(stoppedHubInfo)) {
|
|
@@ -803,23 +1044,23 @@ function cmdUpdate() {
|
|
|
803
1044
|
fail(`업데이트 실패: ${e.message}`);
|
|
804
1045
|
return;
|
|
805
1046
|
}
|
|
806
|
-
|
|
807
|
-
// 3. setup 재실행 (tfx-route.sh, HUD, 스킬 동기화)
|
|
808
|
-
if (updated) {
|
|
809
|
-
console.log("");
|
|
810
|
-
// 업데이트 후 새 버전 읽기
|
|
811
|
-
let newVer = oldVer;
|
|
812
|
-
try {
|
|
813
|
-
const newPkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
814
|
-
newVer = newPkg.version;
|
|
815
|
-
} catch {}
|
|
816
|
-
|
|
817
|
-
if (newVer !== oldVer) {
|
|
818
|
-
ok(`버전: v${oldVer} → v${newVer}`);
|
|
819
|
-
} else {
|
|
820
|
-
ok(`버전: v${oldVer} (이미 최신)`);
|
|
821
|
-
}
|
|
822
|
-
|
|
1047
|
+
|
|
1048
|
+
// 3. setup 재실행 (tfx-route.sh, HUD, 스킬 동기화)
|
|
1049
|
+
if (updated) {
|
|
1050
|
+
console.log("");
|
|
1051
|
+
// 업데이트 후 새 버전 읽기
|
|
1052
|
+
let newVer = oldVer;
|
|
1053
|
+
try {
|
|
1054
|
+
const newPkg = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
|
|
1055
|
+
newVer = newPkg.version;
|
|
1056
|
+
} catch {}
|
|
1057
|
+
|
|
1058
|
+
if (newVer !== oldVer) {
|
|
1059
|
+
ok(`버전: v${oldVer} → v${newVer}`);
|
|
1060
|
+
} else {
|
|
1061
|
+
ok(`버전: v${oldVer} (이미 최신)`);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
823
1064
|
// setup 재실행
|
|
824
1065
|
console.log("");
|
|
825
1066
|
info("setup 재실행 중...");
|
|
@@ -830,155 +1071,155 @@ function cmdUpdate() {
|
|
|
830
1071
|
else warn("hub 재기동 실패 — `tfx hub start`로 수동 시작 필요");
|
|
831
1072
|
}
|
|
832
1073
|
}
|
|
833
|
-
|
|
834
|
-
console.log(`${GREEN}${BOLD}업데이트 완료${RESET}\n`);
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
function cmdList() {
|
|
838
|
-
console.log(`\n ${AMBER}${BOLD}⬡ triflux list${RESET} ${VER}\n`);
|
|
839
|
-
console.log(` ${LINE}`);
|
|
840
|
-
|
|
841
|
-
const pluginSkills = join(PKG_ROOT, "skills");
|
|
842
|
-
const installedSkills = join(CLAUDE_DIR, "skills");
|
|
843
|
-
|
|
844
|
-
section("패키지 스킬");
|
|
845
|
-
if (existsSync(pluginSkills)) {
|
|
846
|
-
for (const name of readdirSync(pluginSkills).sort()) {
|
|
847
|
-
const src = join(pluginSkills, name, "SKILL.md");
|
|
848
|
-
if (!existsSync(src)) continue;
|
|
849
|
-
const dst = join(installedSkills, name, "SKILL.md");
|
|
850
|
-
const installed = existsSync(dst);
|
|
851
|
-
if (installed) {
|
|
852
|
-
console.log(` ${GREEN_BRIGHT}✓${RESET} ${BOLD}${name}${RESET}`);
|
|
853
|
-
} else {
|
|
854
|
-
console.log(` ${RED_BRIGHT}✗${RESET} ${DIM}${name}${RESET} ${GRAY}(미설치)${RESET}`);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
section("사용자 스킬");
|
|
860
|
-
const pkgNames = new Set(existsSync(pluginSkills) ? readdirSync(pluginSkills) : []);
|
|
861
|
-
let userCount = 0;
|
|
862
|
-
if (existsSync(installedSkills)) {
|
|
863
|
-
for (const name of readdirSync(installedSkills).sort()) {
|
|
864
|
-
if (pkgNames.has(name)) continue;
|
|
865
|
-
const skill = join(installedSkills, name, "SKILL.md");
|
|
866
|
-
if (!existsSync(skill)) continue;
|
|
867
|
-
console.log(` ${AMBER}◆${RESET} ${name}`);
|
|
868
|
-
userCount++;
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
if (userCount === 0) console.log(` ${GRAY}없음${RESET}`);
|
|
872
|
-
|
|
873
|
-
console.log(`\n ${LINE}`);
|
|
874
|
-
console.log(` ${GRAY}${installedSkills}${RESET}\n`);
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
function cmdVersion() {
|
|
878
|
-
const routeVer = getVersion(join(CLAUDE_DIR, "scripts", "tfx-route.sh"));
|
|
879
|
-
const hudVer = getVersion(join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"));
|
|
880
|
-
console.log(`\n ${AMBER}${BOLD}⬡ triflux${RESET} ${WHITE_BRIGHT}v${PKG.version}${RESET}`);
|
|
881
|
-
if (routeVer) console.log(` ${GRAY}tfx-route${RESET} v${routeVer}`);
|
|
882
|
-
if (hudVer) console.log(` ${GRAY}hud${RESET} v${hudVer}`);
|
|
883
|
-
console.log("");
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
function checkForUpdate() {
|
|
887
|
-
const cacheFile = join(CLAUDE_DIR, "cache", "triflux-update-check.json");
|
|
888
|
-
const cacheDir = dirname(cacheFile);
|
|
889
|
-
|
|
890
|
-
// 캐시 확인 (1시간 이내면 캐시 사용)
|
|
891
|
-
try {
|
|
892
|
-
if (existsSync(cacheFile)) {
|
|
893
|
-
const cache = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
894
|
-
if (Date.now() - cache.timestamp < 3600000) {
|
|
895
|
-
return cache.latest !== PKG.version ? cache.latest : null;
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
} catch {}
|
|
899
|
-
|
|
900
|
-
// npm registry 조회
|
|
901
|
-
try {
|
|
902
|
-
const result = execSync("npm view triflux version", {
|
|
903
|
-
encoding: "utf8",
|
|
904
|
-
timeout: 5000,
|
|
905
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
906
|
-
}).trim();
|
|
907
|
-
|
|
908
|
-
if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true });
|
|
909
|
-
writeFileSync(cacheFile, JSON.stringify({ latest: result, timestamp: Date.now() }));
|
|
910
|
-
|
|
911
|
-
return result !== PKG.version ? result : null;
|
|
912
|
-
} catch {
|
|
913
|
-
return null;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
function cmdHelp() {
|
|
918
|
-
const latestVer = checkForUpdate();
|
|
919
|
-
const updateNotice = latestVer
|
|
920
|
-
? `\n ${YELLOW}${BOLD}↑ v${latestVer} 사용 가능${RESET} ${GRAY}npm update -g triflux${RESET}\n`
|
|
921
|
-
: "";
|
|
922
|
-
|
|
923
|
-
console.log(`
|
|
924
|
-
${AMBER}${BOLD}⬡ triflux${RESET} ${DIM}v${PKG.version}${RESET}
|
|
925
|
-
${GRAY}CLI-first multi-model orchestrator for Claude Code${RESET}
|
|
926
|
-
${updateNotice}
|
|
927
|
-
${LINE}
|
|
928
|
-
|
|
929
|
-
${BOLD}Commands${RESET}
|
|
930
|
-
|
|
931
|
-
${WHITE_BRIGHT}tfx setup${RESET} ${GRAY}파일 동기화 + HUD 설정${RESET}
|
|
932
|
-
${WHITE_BRIGHT}tfx doctor${RESET} ${GRAY}CLI 진단 + 이슈 확인${RESET}
|
|
933
|
-
${DIM} --fix${RESET} ${GRAY}진단 + 자동 수정${RESET}
|
|
934
|
-
${DIM} --reset${RESET} ${GRAY}캐시 전체 초기화${RESET}
|
|
1074
|
+
|
|
1075
|
+
console.log(`${GREEN}${BOLD}업데이트 완료${RESET}\n`);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function cmdList() {
|
|
1079
|
+
console.log(`\n ${AMBER}${BOLD}⬡ triflux list${RESET} ${VER}\n`);
|
|
1080
|
+
console.log(` ${LINE}`);
|
|
1081
|
+
|
|
1082
|
+
const pluginSkills = join(PKG_ROOT, "skills");
|
|
1083
|
+
const installedSkills = join(CLAUDE_DIR, "skills");
|
|
1084
|
+
|
|
1085
|
+
section("패키지 스킬");
|
|
1086
|
+
if (existsSync(pluginSkills)) {
|
|
1087
|
+
for (const name of readdirSync(pluginSkills).sort()) {
|
|
1088
|
+
const src = join(pluginSkills, name, "SKILL.md");
|
|
1089
|
+
if (!existsSync(src)) continue;
|
|
1090
|
+
const dst = join(installedSkills, name, "SKILL.md");
|
|
1091
|
+
const installed = existsSync(dst);
|
|
1092
|
+
if (installed) {
|
|
1093
|
+
console.log(` ${GREEN_BRIGHT}✓${RESET} ${BOLD}${name}${RESET}`);
|
|
1094
|
+
} else {
|
|
1095
|
+
console.log(` ${RED_BRIGHT}✗${RESET} ${DIM}${name}${RESET} ${GRAY}(미설치)${RESET}`);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
section("사용자 스킬");
|
|
1101
|
+
const pkgNames = new Set(existsSync(pluginSkills) ? readdirSync(pluginSkills) : []);
|
|
1102
|
+
let userCount = 0;
|
|
1103
|
+
if (existsSync(installedSkills)) {
|
|
1104
|
+
for (const name of readdirSync(installedSkills).sort()) {
|
|
1105
|
+
if (pkgNames.has(name)) continue;
|
|
1106
|
+
const skill = join(installedSkills, name, "SKILL.md");
|
|
1107
|
+
if (!existsSync(skill)) continue;
|
|
1108
|
+
console.log(` ${AMBER}◆${RESET} ${name}`);
|
|
1109
|
+
userCount++;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
if (userCount === 0) console.log(` ${GRAY}없음${RESET}`);
|
|
1113
|
+
|
|
1114
|
+
console.log(`\n ${LINE}`);
|
|
1115
|
+
console.log(` ${GRAY}${installedSkills}${RESET}\n`);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function cmdVersion() {
|
|
1119
|
+
const routeVer = getVersion(join(CLAUDE_DIR, "scripts", "tfx-route.sh"));
|
|
1120
|
+
const hudVer = getVersion(join(CLAUDE_DIR, "hud", "hud-qos-status.mjs"));
|
|
1121
|
+
console.log(`\n ${AMBER}${BOLD}⬡ triflux${RESET} ${WHITE_BRIGHT}v${PKG.version}${RESET}`);
|
|
1122
|
+
if (routeVer) console.log(` ${GRAY}tfx-route${RESET} v${routeVer}`);
|
|
1123
|
+
if (hudVer) console.log(` ${GRAY}hud${RESET} v${hudVer}`);
|
|
1124
|
+
console.log("");
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function checkForUpdate() {
|
|
1128
|
+
const cacheFile = join(CLAUDE_DIR, "cache", "triflux-update-check.json");
|
|
1129
|
+
const cacheDir = dirname(cacheFile);
|
|
1130
|
+
|
|
1131
|
+
// 캐시 확인 (1시간 이내면 캐시 사용)
|
|
1132
|
+
try {
|
|
1133
|
+
if (existsSync(cacheFile)) {
|
|
1134
|
+
const cache = JSON.parse(readFileSync(cacheFile, "utf8"));
|
|
1135
|
+
if (Date.now() - cache.timestamp < 3600000) {
|
|
1136
|
+
return cache.latest !== PKG.version ? cache.latest : null;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
} catch {}
|
|
1140
|
+
|
|
1141
|
+
// npm registry 조회
|
|
1142
|
+
try {
|
|
1143
|
+
const result = execSync("npm view triflux version", {
|
|
1144
|
+
encoding: "utf8",
|
|
1145
|
+
timeout: 5000,
|
|
1146
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
1147
|
+
}).trim();
|
|
1148
|
+
|
|
1149
|
+
if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true });
|
|
1150
|
+
writeFileSync(cacheFile, JSON.stringify({ latest: result, timestamp: Date.now() }));
|
|
1151
|
+
|
|
1152
|
+
return result !== PKG.version ? result : null;
|
|
1153
|
+
} catch {
|
|
1154
|
+
return null;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function cmdHelp() {
|
|
1159
|
+
const latestVer = checkForUpdate();
|
|
1160
|
+
const updateNotice = latestVer
|
|
1161
|
+
? `\n ${YELLOW}${BOLD}↑ v${latestVer} 사용 가능${RESET} ${GRAY}npm update -g triflux${RESET}\n`
|
|
1162
|
+
: "";
|
|
1163
|
+
|
|
1164
|
+
console.log(`
|
|
1165
|
+
${AMBER}${BOLD}⬡ triflux${RESET} ${DIM}v${PKG.version}${RESET}
|
|
1166
|
+
${GRAY}CLI-first multi-model orchestrator for Claude Code${RESET}
|
|
1167
|
+
${updateNotice}
|
|
1168
|
+
${LINE}
|
|
1169
|
+
|
|
1170
|
+
${BOLD}Commands${RESET}
|
|
1171
|
+
|
|
1172
|
+
${WHITE_BRIGHT}tfx setup${RESET} ${GRAY}파일 동기화 + HUD 설정${RESET}
|
|
1173
|
+
${WHITE_BRIGHT}tfx doctor${RESET} ${GRAY}CLI 진단 + 이슈 확인${RESET}
|
|
1174
|
+
${DIM} --fix${RESET} ${GRAY}진단 + 자동 수정${RESET}
|
|
1175
|
+
${DIM} --reset${RESET} ${GRAY}캐시 전체 초기화${RESET}
|
|
935
1176
|
${WHITE_BRIGHT}tfx update${RESET} ${GRAY}최신 안정 버전으로 업데이트${RESET}
|
|
936
1177
|
${DIM} --dev / dev${RESET} ${GRAY}dev 태그로 업데이트${RESET}
|
|
937
|
-
${WHITE_BRIGHT}tfx list${RESET} ${GRAY}설치된 스킬 목록${RESET}
|
|
938
|
-
${WHITE_BRIGHT}tfx hub${RESET} ${GRAY}MCP 메시지 버스 관리 (start/stop/status)${RESET}
|
|
939
|
-
${WHITE_BRIGHT}tfx
|
|
940
|
-
${WHITE_BRIGHT}tfx codex-team${RESET} ${GRAY}Codex 전용 팀 모드 (기본 lead/agents: codex)${RESET}
|
|
941
|
-
${WHITE_BRIGHT}tfx notion-read${RESET} ${GRAY}Notion 페이지 → 마크다운 (Codex/Gemini MCP)${RESET}
|
|
942
|
-
${WHITE_BRIGHT}tfx version${RESET} ${GRAY}버전 표시${RESET}
|
|
943
|
-
|
|
1178
|
+
${WHITE_BRIGHT}tfx list${RESET} ${GRAY}설치된 스킬 목록${RESET}
|
|
1179
|
+
${WHITE_BRIGHT}tfx hub${RESET} ${GRAY}MCP 메시지 버스 관리 (start/stop/status)${RESET}
|
|
1180
|
+
${WHITE_BRIGHT}tfx multi${RESET} ${GRAY}멀티-CLI 팀 모드 (tmux + Hub)${RESET}
|
|
1181
|
+
${WHITE_BRIGHT}tfx codex-team${RESET} ${GRAY}Codex 전용 팀 모드 (기본 lead/agents: codex)${RESET}
|
|
1182
|
+
${WHITE_BRIGHT}tfx notion-read${RESET} ${GRAY}Notion 페이지 → 마크다운 (Codex/Gemini MCP)${RESET}
|
|
1183
|
+
${WHITE_BRIGHT}tfx version${RESET} ${GRAY}버전 표시${RESET}
|
|
1184
|
+
|
|
944
1185
|
${BOLD}Skills${RESET} ${GRAY}(Claude Code 슬래시 커맨드)${RESET}
|
|
945
1186
|
|
|
946
1187
|
${AMBER}/tfx-auto${RESET} ${GRAY}자동 분류 + 병렬 실행${RESET}
|
|
947
1188
|
${WHITE_BRIGHT}/tfx-auto-codex${RESET} ${GRAY}Codex 리드 + Gemini 유지 (no-Claude-native)${RESET}
|
|
948
1189
|
${WHITE_BRIGHT}/tfx-codex${RESET} ${GRAY}Codex 전용 모드${RESET}
|
|
949
1190
|
${BLUE}/tfx-gemini${RESET} ${GRAY}Gemini 전용 모드${RESET}
|
|
950
|
-
${AMBER}/tfx-setup${RESET} ${GRAY}HUD 설정 + 진단${RESET}
|
|
951
|
-
${YELLOW}/tfx-doctor${RESET} ${GRAY}진단 + 수리 + 캐시 초기화${RESET}
|
|
952
|
-
|
|
953
|
-
${LINE}
|
|
954
|
-
${GRAY}github.com/tellang/triflux${RESET}
|
|
955
|
-
`);
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
async function cmdCodexTeam() {
|
|
959
|
-
const args = process.argv.slice(3);
|
|
960
|
-
const sub = String(args[0] || "").toLowerCase();
|
|
961
|
-
const passthrough = new Set([
|
|
962
|
-
"status", "attach", "stop", "kill", "send", "list", "help", "--help", "-h",
|
|
963
|
-
"tasks", "task", "focus", "interrupt", "control", "debug",
|
|
964
|
-
]);
|
|
965
|
-
|
|
966
|
-
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
967
|
-
console.log(`
|
|
968
|
-
${AMBER}${BOLD}⬡ tfx codex-team${RESET}
|
|
969
|
-
|
|
970
|
-
${WHITE_BRIGHT}tfx codex-team "작업"${RESET} ${GRAY}Codex 리드 + 워커 2개로 팀 시작${RESET}
|
|
971
|
-
${WHITE_BRIGHT}tfx codex-team --layout 1xN "작업"${RESET} ${GRAY}(세로 분할 컬럼)${RESET}
|
|
972
|
-
${WHITE_BRIGHT}tfx codex-team --layout Nx1 "작업"${RESET} ${GRAY}(가로 분할 스택)${RESET}
|
|
973
|
-
${WHITE_BRIGHT}tfx codex-team status${RESET}
|
|
974
|
-
${WHITE_BRIGHT}tfx codex-team debug --lines 30${RESET}
|
|
975
|
-
${WHITE_BRIGHT}tfx codex-team send N "msg"${RESET}
|
|
976
|
-
|
|
977
|
-
${DIM}내부적으로 tfx
|
|
978
|
-
`);
|
|
979
|
-
return;
|
|
980
|
-
}
|
|
981
|
-
|
|
1191
|
+
${AMBER}/tfx-setup${RESET} ${GRAY}HUD 설정 + 진단${RESET}
|
|
1192
|
+
${YELLOW}/tfx-doctor${RESET} ${GRAY}진단 + 수리 + 캐시 초기화${RESET}
|
|
1193
|
+
|
|
1194
|
+
${LINE}
|
|
1195
|
+
${GRAY}github.com/tellang/triflux${RESET}
|
|
1196
|
+
`);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
async function cmdCodexTeam() {
|
|
1200
|
+
const args = process.argv.slice(3);
|
|
1201
|
+
const sub = String(args[0] || "").toLowerCase();
|
|
1202
|
+
const passthrough = new Set([
|
|
1203
|
+
"status", "attach", "stop", "kill", "send", "list", "help", "--help", "-h",
|
|
1204
|
+
"tasks", "task", "focus", "interrupt", "control", "debug",
|
|
1205
|
+
]);
|
|
1206
|
+
|
|
1207
|
+
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
1208
|
+
console.log(`
|
|
1209
|
+
${AMBER}${BOLD}⬡ tfx codex-team${RESET}
|
|
1210
|
+
|
|
1211
|
+
${WHITE_BRIGHT}tfx codex-team "작업"${RESET} ${GRAY}Codex 리드 + 워커 2개로 팀 시작${RESET}
|
|
1212
|
+
${WHITE_BRIGHT}tfx codex-team --layout 1xN "작업"${RESET} ${GRAY}(세로 분할 컬럼)${RESET}
|
|
1213
|
+
${WHITE_BRIGHT}tfx codex-team --layout Nx1 "작업"${RESET} ${GRAY}(가로 분할 스택)${RESET}
|
|
1214
|
+
${WHITE_BRIGHT}tfx codex-team status${RESET}
|
|
1215
|
+
${WHITE_BRIGHT}tfx codex-team debug --lines 30${RESET}
|
|
1216
|
+
${WHITE_BRIGHT}tfx codex-team send N "msg"${RESET}
|
|
1217
|
+
|
|
1218
|
+
${DIM}내부적으로 tfx multi을 호출하며, 시작 시 --lead codex --agents codex,codex를 기본 주입합니다.${RESET}
|
|
1219
|
+
`);
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
982
1223
|
const hasAgents = args.includes("--agents");
|
|
983
1224
|
const hasLead = args.includes("--lead");
|
|
984
1225
|
const hasLayout = args.includes("--layout");
|
|
@@ -989,7 +1230,7 @@ async function cmdCodexTeam() {
|
|
|
989
1230
|
if (!isControl && !hasAgents) inject.push("--agents", "codex,codex");
|
|
990
1231
|
if (!isControl && !hasLayout) inject.push("--layout", "1xN");
|
|
991
1232
|
const forwarded = isControl ? normalizedArgs : [...inject, ...args];
|
|
992
|
-
|
|
1233
|
+
|
|
993
1234
|
const prevArgv = process.argv;
|
|
994
1235
|
const prevProfile = process.env.TFX_TEAM_PROFILE;
|
|
995
1236
|
process.env.TFX_TEAM_PROFILE = "codex-team";
|
|
@@ -1004,9 +1245,9 @@ async function cmdCodexTeam() {
|
|
|
1004
1245
|
else delete process.env.TFX_TEAM_PROFILE;
|
|
1005
1246
|
}
|
|
1006
1247
|
}
|
|
1007
|
-
|
|
1008
|
-
// ── hub 서브커맨드 ──
|
|
1009
|
-
|
|
1248
|
+
|
|
1249
|
+
// ── hub 서브커맨드 ──
|
|
1250
|
+
|
|
1010
1251
|
const HUB_PID_DIR = join(homedir(), ".claude", "cache", "tfx-hub");
|
|
1011
1252
|
const HUB_PID_FILE = join(HUB_PID_DIR, "hub.pid");
|
|
1012
1253
|
|
|
@@ -1061,81 +1302,81 @@ function startHubAfterUpdate(info) {
|
|
|
1061
1302
|
return false;
|
|
1062
1303
|
}
|
|
1063
1304
|
}
|
|
1064
|
-
|
|
1065
|
-
// 설치된 CLI에 tfx-hub MCP 서버 자동 등록 (1회 설정, 이후 재실행 불필요)
|
|
1066
|
-
function autoRegisterMcp(mcpUrl) {
|
|
1067
|
-
section("MCP 자동 등록");
|
|
1068
|
-
|
|
1069
|
-
// Codex — codex mcp add
|
|
1070
|
-
if (which("codex")) {
|
|
1071
|
-
try {
|
|
1072
|
-
// 이미 등록됐는지 확인
|
|
1073
|
-
const list = execSync("codex mcp list 2>&1", { encoding: "utf8", timeout: 10000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1074
|
-
if (list.includes("tfx-hub")) {
|
|
1075
|
-
ok("Codex: 이미 등록됨");
|
|
1076
|
-
} else {
|
|
1077
|
-
execSync(`codex mcp add tfx-hub --url ${mcpUrl}`, { timeout: 10000, stdio: "ignore" });
|
|
1078
|
-
ok("Codex: MCP 등록 완료");
|
|
1079
|
-
}
|
|
1080
|
-
} catch {
|
|
1081
|
-
// mcp list/add 미지원 → 설정 파일 직접 수정
|
|
1082
|
-
try {
|
|
1083
|
-
const codexDir = join(homedir(), ".codex");
|
|
1084
|
-
const configFile = join(codexDir, "config.json");
|
|
1085
|
-
let config = {};
|
|
1086
|
-
if (existsSync(configFile)) config = JSON.parse(readFileSync(configFile, "utf8"));
|
|
1087
|
-
if (!config.mcpServers) config.mcpServers = {};
|
|
1088
|
-
if (!config.mcpServers["tfx-hub"]) {
|
|
1089
|
-
config.mcpServers["tfx-hub"] = { url: mcpUrl };
|
|
1090
|
-
if (!existsSync(codexDir)) mkdirSync(codexDir, { recursive: true });
|
|
1091
|
-
writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n");
|
|
1092
|
-
ok("Codex: config.json에 등록 완료");
|
|
1093
|
-
} else {
|
|
1094
|
-
ok("Codex: 이미 등록됨");
|
|
1095
|
-
}
|
|
1096
|
-
} catch (e) { warn(`Codex 등록 실패: ${e.message}`); }
|
|
1097
|
-
}
|
|
1098
|
-
} else {
|
|
1099
|
-
info("Codex: 미설치 (건너뜀)");
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
// Gemini — settings.json 직접 수정
|
|
1103
|
-
if (which("gemini")) {
|
|
1104
|
-
try {
|
|
1105
|
-
const geminiDir = join(homedir(), ".gemini");
|
|
1106
|
-
const settingsFile = join(geminiDir, "settings.json");
|
|
1107
|
-
let settings = {};
|
|
1108
|
-
if (existsSync(settingsFile)) settings = JSON.parse(readFileSync(settingsFile, "utf8"));
|
|
1109
|
-
if (!settings.mcpServers) settings.mcpServers = {};
|
|
1110
|
-
if (!settings.mcpServers["tfx-hub"]) {
|
|
1111
|
-
settings.mcpServers["tfx-hub"] = { url: mcpUrl };
|
|
1112
|
-
if (!existsSync(geminiDir)) mkdirSync(geminiDir, { recursive: true });
|
|
1113
|
-
writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
|
|
1114
|
-
ok("Gemini: settings.json에 등록 완료");
|
|
1115
|
-
} else {
|
|
1116
|
-
ok("Gemini: 이미 등록됨");
|
|
1117
|
-
}
|
|
1118
|
-
} catch (e) { warn(`Gemini 등록 실패: ${e.message}`); }
|
|
1119
|
-
} else {
|
|
1120
|
-
info("Gemini: 미설치 (건너뜀)");
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
// Claude — 프로젝트 .mcp.json에 등록 (오케스트레이터용)
|
|
1124
|
-
try {
|
|
1125
|
-
const mcpJsonPath = join(PKG_ROOT, ".mcp.json");
|
|
1126
|
-
let mcpJson = {};
|
|
1127
|
-
if (existsSync(mcpJsonPath)) mcpJson = JSON.parse(readFileSync(mcpJsonPath, "utf8"));
|
|
1128
|
-
if (!mcpJson.mcpServers) mcpJson.mcpServers = {};
|
|
1129
|
-
if (!mcpJson.mcpServers["tfx-hub"]) {
|
|
1130
|
-
mcpJson.mcpServers["tfx-hub"] = { type: "url", url: mcpUrl };
|
|
1131
|
-
writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + "\n");
|
|
1132
|
-
ok("Claude: .mcp.json에 등록 완료");
|
|
1133
|
-
} else {
|
|
1134
|
-
ok("Claude: 이미 등록됨");
|
|
1135
|
-
}
|
|
1136
|
-
} catch (e) { warn(`Claude 등록 실패: ${e.message}`); }
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1305
|
+
|
|
1306
|
+
// 설치된 CLI에 tfx-hub MCP 서버 자동 등록 (1회 설정, 이후 재실행 불필요)
|
|
1307
|
+
function autoRegisterMcp(mcpUrl) {
|
|
1308
|
+
section("MCP 자동 등록");
|
|
1309
|
+
|
|
1310
|
+
// Codex — codex mcp add
|
|
1311
|
+
if (which("codex")) {
|
|
1312
|
+
try {
|
|
1313
|
+
// 이미 등록됐는지 확인
|
|
1314
|
+
const list = execSync("codex mcp list 2>&1", { encoding: "utf8", timeout: 10000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1315
|
+
if (list.includes("tfx-hub")) {
|
|
1316
|
+
ok("Codex: 이미 등록됨");
|
|
1317
|
+
} else {
|
|
1318
|
+
execSync(`codex mcp add tfx-hub --url ${mcpUrl}`, { timeout: 10000, stdio: "ignore" });
|
|
1319
|
+
ok("Codex: MCP 등록 완료");
|
|
1320
|
+
}
|
|
1321
|
+
} catch {
|
|
1322
|
+
// mcp list/add 미지원 → 설정 파일 직접 수정
|
|
1323
|
+
try {
|
|
1324
|
+
const codexDir = join(homedir(), ".codex");
|
|
1325
|
+
const configFile = join(codexDir, "config.json");
|
|
1326
|
+
let config = {};
|
|
1327
|
+
if (existsSync(configFile)) config = JSON.parse(readFileSync(configFile, "utf8"));
|
|
1328
|
+
if (!config.mcpServers) config.mcpServers = {};
|
|
1329
|
+
if (!config.mcpServers["tfx-hub"]) {
|
|
1330
|
+
config.mcpServers["tfx-hub"] = { url: mcpUrl };
|
|
1331
|
+
if (!existsSync(codexDir)) mkdirSync(codexDir, { recursive: true });
|
|
1332
|
+
writeFileSync(configFile, JSON.stringify(config, null, 2) + "\n");
|
|
1333
|
+
ok("Codex: config.json에 등록 완료");
|
|
1334
|
+
} else {
|
|
1335
|
+
ok("Codex: 이미 등록됨");
|
|
1336
|
+
}
|
|
1337
|
+
} catch (e) { warn(`Codex 등록 실패: ${e.message}`); }
|
|
1338
|
+
}
|
|
1339
|
+
} else {
|
|
1340
|
+
info("Codex: 미설치 (건너뜀)");
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// Gemini — settings.json 직접 수정
|
|
1344
|
+
if (which("gemini")) {
|
|
1345
|
+
try {
|
|
1346
|
+
const geminiDir = join(homedir(), ".gemini");
|
|
1347
|
+
const settingsFile = join(geminiDir, "settings.json");
|
|
1348
|
+
let settings = {};
|
|
1349
|
+
if (existsSync(settingsFile)) settings = JSON.parse(readFileSync(settingsFile, "utf8"));
|
|
1350
|
+
if (!settings.mcpServers) settings.mcpServers = {};
|
|
1351
|
+
if (!settings.mcpServers["tfx-hub"]) {
|
|
1352
|
+
settings.mcpServers["tfx-hub"] = { url: mcpUrl };
|
|
1353
|
+
if (!existsSync(geminiDir)) mkdirSync(geminiDir, { recursive: true });
|
|
1354
|
+
writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
|
|
1355
|
+
ok("Gemini: settings.json에 등록 완료");
|
|
1356
|
+
} else {
|
|
1357
|
+
ok("Gemini: 이미 등록됨");
|
|
1358
|
+
}
|
|
1359
|
+
} catch (e) { warn(`Gemini 등록 실패: ${e.message}`); }
|
|
1360
|
+
} else {
|
|
1361
|
+
info("Gemini: 미설치 (건너뜀)");
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Claude — 프로젝트 .mcp.json에 등록 (오케스트레이터용)
|
|
1365
|
+
try {
|
|
1366
|
+
const mcpJsonPath = join(PKG_ROOT, ".mcp.json");
|
|
1367
|
+
let mcpJson = {};
|
|
1368
|
+
if (existsSync(mcpJsonPath)) mcpJson = JSON.parse(readFileSync(mcpJsonPath, "utf8"));
|
|
1369
|
+
if (!mcpJson.mcpServers) mcpJson.mcpServers = {};
|
|
1370
|
+
if (!mcpJson.mcpServers["tfx-hub"]) {
|
|
1371
|
+
mcpJson.mcpServers["tfx-hub"] = { type: "url", url: mcpUrl };
|
|
1372
|
+
writeFileSync(mcpJsonPath, JSON.stringify(mcpJson, null, 2) + "\n");
|
|
1373
|
+
ok("Claude: .mcp.json에 등록 완료");
|
|
1374
|
+
} else {
|
|
1375
|
+
ok("Claude: 이미 등록됨");
|
|
1376
|
+
}
|
|
1377
|
+
} catch (e) { warn(`Claude 등록 실패: ${e.message}`); }
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1139
1380
|
async function cmdHub() {
|
|
1140
1381
|
const sub = process.argv[3] || "status";
|
|
1141
1382
|
const defaultPortRaw = Number(process.env.TFX_HUB_PORT || "27888");
|
|
@@ -1170,36 +1411,36 @@ async function cmdHub() {
|
|
|
1170
1411
|
};
|
|
1171
1412
|
|
|
1172
1413
|
switch (sub) {
|
|
1173
|
-
case "start": {
|
|
1174
|
-
// 이미 실행 중인지 확인
|
|
1175
|
-
if (existsSync(HUB_PID_FILE)) {
|
|
1176
|
-
try {
|
|
1177
|
-
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1178
|
-
process.kill(info.pid, 0); // 프로세스 존재 확인
|
|
1179
|
-
console.log(`\n ${YELLOW}⚠${RESET} hub 이미 실행 중 (PID ${info.pid}, ${info.url})\n`);
|
|
1180
|
-
return;
|
|
1181
|
-
} catch {
|
|
1182
|
-
// PID 파일 있지만 프로세스 없음 — 정리
|
|
1183
|
-
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
const portArg = process.argv.indexOf("--port");
|
|
1188
|
-
const port = portArg !== -1 ? process.argv[portArg + 1] : "27888";
|
|
1189
|
-
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
1190
|
-
|
|
1191
|
-
if (!existsSync(serverPath)) {
|
|
1192
|
-
fail("hub/server.mjs 없음 — hub 모듈이 설치되지 않음");
|
|
1193
|
-
return;
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
const child = spawn(process.execPath, [serverPath], {
|
|
1197
|
-
env: { ...process.env, TFX_HUB_PORT: port },
|
|
1198
|
-
stdio: "ignore",
|
|
1199
|
-
detached: true,
|
|
1200
|
-
});
|
|
1201
|
-
child.unref();
|
|
1202
|
-
|
|
1414
|
+
case "start": {
|
|
1415
|
+
// 이미 실행 중인지 확인
|
|
1416
|
+
if (existsSync(HUB_PID_FILE)) {
|
|
1417
|
+
try {
|
|
1418
|
+
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1419
|
+
process.kill(info.pid, 0); // 프로세스 존재 확인
|
|
1420
|
+
console.log(`\n ${YELLOW}⚠${RESET} hub 이미 실행 중 (PID ${info.pid}, ${info.url})\n`);
|
|
1421
|
+
return;
|
|
1422
|
+
} catch {
|
|
1423
|
+
// PID 파일 있지만 프로세스 없음 — 정리
|
|
1424
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
const portArg = process.argv.indexOf("--port");
|
|
1429
|
+
const port = portArg !== -1 ? process.argv[portArg + 1] : "27888";
|
|
1430
|
+
const serverPath = join(PKG_ROOT, "hub", "server.mjs");
|
|
1431
|
+
|
|
1432
|
+
if (!existsSync(serverPath)) {
|
|
1433
|
+
fail("hub/server.mjs 없음 — hub 모듈이 설치되지 않음");
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
1438
|
+
env: { ...process.env, TFX_HUB_PORT: port },
|
|
1439
|
+
stdio: "ignore",
|
|
1440
|
+
detached: true,
|
|
1441
|
+
});
|
|
1442
|
+
child.unref();
|
|
1443
|
+
|
|
1203
1444
|
// PID 파일 확인 (최대 3초 대기, 100ms 폴링)
|
|
1204
1445
|
let started = false;
|
|
1205
1446
|
const deadline = Date.now() + 3000;
|
|
@@ -1207,24 +1448,24 @@ async function cmdHub() {
|
|
|
1207
1448
|
if (existsSync(HUB_PID_FILE)) { started = true; break; }
|
|
1208
1449
|
await new Promise((r) => setTimeout(r, 100));
|
|
1209
1450
|
}
|
|
1210
|
-
|
|
1211
|
-
if (started) {
|
|
1212
|
-
const hubInfo = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1213
|
-
console.log(`\n ${GREEN_BRIGHT}✓${RESET} ${BOLD}tfx-hub 시작${RESET}`);
|
|
1214
|
-
console.log(` URL: ${AMBER}${hubInfo.url}${RESET}`);
|
|
1215
|
-
console.log(` PID: ${hubInfo.pid}`);
|
|
1216
|
-
console.log(` DB: ${DIM}${HUB_PID_DIR}/state.db${RESET}`);
|
|
1217
|
-
console.log("");
|
|
1218
|
-
autoRegisterMcp(hubInfo.url);
|
|
1219
|
-
console.log("");
|
|
1220
|
-
} else {
|
|
1221
|
-
// 직접 포그라운드 모드로 안내
|
|
1222
|
-
console.log(`\n ${YELLOW}⚠${RESET} 백그라운드 시작 실패 — 포그라운드로 실행:`);
|
|
1223
|
-
console.log(` ${DIM}TFX_HUB_PORT=${port} node ${serverPath}${RESET}\n`);
|
|
1224
|
-
}
|
|
1225
|
-
break;
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1451
|
+
|
|
1452
|
+
if (started) {
|
|
1453
|
+
const hubInfo = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1454
|
+
console.log(`\n ${GREEN_BRIGHT}✓${RESET} ${BOLD}tfx-hub 시작${RESET}`);
|
|
1455
|
+
console.log(` URL: ${AMBER}${hubInfo.url}${RESET}`);
|
|
1456
|
+
console.log(` PID: ${hubInfo.pid}`);
|
|
1457
|
+
console.log(` DB: ${DIM}${HUB_PID_DIR}/state.db${RESET}`);
|
|
1458
|
+
console.log("");
|
|
1459
|
+
autoRegisterMcp(hubInfo.url);
|
|
1460
|
+
console.log("");
|
|
1461
|
+
} else {
|
|
1462
|
+
// 직접 포그라운드 모드로 안내
|
|
1463
|
+
console.log(`\n ${YELLOW}⚠${RESET} 백그라운드 시작 실패 — 포그라운드로 실행:`);
|
|
1464
|
+
console.log(` ${DIM}TFX_HUB_PORT=${port} node ${serverPath}${RESET}\n`);
|
|
1465
|
+
}
|
|
1466
|
+
break;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1228
1469
|
case "stop": {
|
|
1229
1470
|
if (!existsSync(HUB_PID_FILE)) {
|
|
1230
1471
|
const probed = await probeHubStatus("127.0.0.1", probePort, 1500)
|
|
@@ -1239,17 +1480,17 @@ async function cmdHub() {
|
|
|
1239
1480
|
console.log(`\n ${DIM}hub 미실행${RESET}\n`);
|
|
1240
1481
|
return;
|
|
1241
1482
|
}
|
|
1242
|
-
try {
|
|
1243
|
-
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1244
|
-
process.kill(info.pid, "SIGTERM");
|
|
1245
|
-
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1246
|
-
console.log(`\n ${GREEN_BRIGHT}✓${RESET} hub 종료됨 (PID ${info.pid})\n`);
|
|
1247
|
-
} catch (e) {
|
|
1248
|
-
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1249
|
-
console.log(`\n ${DIM}hub 프로세스 없음 — PID 파일 정리됨${RESET}\n`);
|
|
1250
|
-
}
|
|
1251
|
-
break;
|
|
1252
|
-
}
|
|
1483
|
+
try {
|
|
1484
|
+
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1485
|
+
process.kill(info.pid, "SIGTERM");
|
|
1486
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1487
|
+
console.log(`\n ${GREEN_BRIGHT}✓${RESET} hub 종료됨 (PID ${info.pid})\n`);
|
|
1488
|
+
} catch (e) {
|
|
1489
|
+
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1490
|
+
console.log(`\n ${DIM}hub 프로세스 없음 — PID 파일 정리됨${RESET}\n`);
|
|
1491
|
+
}
|
|
1492
|
+
break;
|
|
1493
|
+
}
|
|
1253
1494
|
|
|
1254
1495
|
case "status": {
|
|
1255
1496
|
if (!existsSync(HUB_PID_FILE)) {
|
|
@@ -1280,15 +1521,15 @@ async function cmdHub() {
|
|
|
1280
1521
|
}
|
|
1281
1522
|
try {
|
|
1282
1523
|
const info = JSON.parse(readFileSync(HUB_PID_FILE, "utf8"));
|
|
1283
|
-
process.kill(info.pid, 0); // 생존 확인
|
|
1284
|
-
const uptime = Date.now() - info.started;
|
|
1285
|
-
const uptimeStr = uptime < 60000 ? `${Math.round(uptime / 1000)}초`
|
|
1286
|
-
: uptime < 3600000 ? `${Math.round(uptime / 60000)}분`
|
|
1287
|
-
: `${Math.round(uptime / 3600000)}시간`;
|
|
1288
|
-
|
|
1289
|
-
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET}`);
|
|
1290
|
-
console.log(` URL: ${info.url}`);
|
|
1291
|
-
console.log(` PID: ${info.pid}`);
|
|
1524
|
+
process.kill(info.pid, 0); // 생존 확인
|
|
1525
|
+
const uptime = Date.now() - info.started;
|
|
1526
|
+
const uptimeStr = uptime < 60000 ? `${Math.round(uptime / 1000)}초`
|
|
1527
|
+
: uptime < 3600000 ? `${Math.round(uptime / 60000)}분`
|
|
1528
|
+
: `${Math.round(uptime / 3600000)}시간`;
|
|
1529
|
+
|
|
1530
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET} ${GREEN_BRIGHT}online${RESET}`);
|
|
1531
|
+
console.log(` URL: ${info.url}`);
|
|
1532
|
+
console.log(` PID: ${info.pid}`);
|
|
1292
1533
|
console.log(` Uptime: ${uptimeStr}`);
|
|
1293
1534
|
|
|
1294
1535
|
// HTTP 상태 조회 시도
|
|
@@ -1299,12 +1540,12 @@ async function cmdHub() {
|
|
|
1299
1540
|
if (data.hub) {
|
|
1300
1541
|
console.log(` State: ${data.hub.state}`);
|
|
1301
1542
|
}
|
|
1302
|
-
if (data.sessions !== undefined) {
|
|
1303
|
-
console.log(` Sessions: ${data.sessions}`);
|
|
1304
|
-
}
|
|
1305
|
-
} catch {}
|
|
1306
|
-
|
|
1307
|
-
console.log("");
|
|
1543
|
+
if (data.sessions !== undefined) {
|
|
1544
|
+
console.log(` Sessions: ${data.sessions}`);
|
|
1545
|
+
}
|
|
1546
|
+
} catch {}
|
|
1547
|
+
|
|
1548
|
+
console.log("");
|
|
1308
1549
|
} catch {
|
|
1309
1550
|
try { unlinkSync(HUB_PID_FILE); } catch {}
|
|
1310
1551
|
const probed = await probeHubStatus();
|
|
@@ -1322,52 +1563,52 @@ async function cmdHub() {
|
|
|
1322
1563
|
}
|
|
1323
1564
|
break;
|
|
1324
1565
|
}
|
|
1325
|
-
|
|
1326
|
-
default:
|
|
1327
|
-
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET}\n`);
|
|
1328
|
-
console.log(` ${WHITE_BRIGHT}tfx hub start${RESET} ${GRAY}허브 데몬 시작${RESET}`);
|
|
1329
|
-
console.log(` ${DIM} --port N${RESET} ${GRAY}포트 지정 (기본 27888)${RESET}`);
|
|
1330
|
-
console.log(` ${WHITE_BRIGHT}tfx hub stop${RESET} ${GRAY}허브 중지${RESET}`);
|
|
1331
|
-
console.log(` ${WHITE_BRIGHT}tfx hub status${RESET} ${GRAY}상태 확인${RESET}\n`);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
// ── 메인 ──
|
|
1336
|
-
|
|
1337
|
-
const cmd = process.argv[2] || "help";
|
|
1338
|
-
|
|
1339
|
-
switch (cmd) {
|
|
1340
|
-
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
|
-
}
|
|
1347
|
-
case "update": cmdUpdate(); break;
|
|
1348
|
-
case "list": case "ls": cmdList(); break;
|
|
1566
|
+
|
|
1567
|
+
default:
|
|
1568
|
+
console.log(`\n ${AMBER}${BOLD}⬡ tfx-hub${RESET}\n`);
|
|
1569
|
+
console.log(` ${WHITE_BRIGHT}tfx hub start${RESET} ${GRAY}허브 데몬 시작${RESET}`);
|
|
1570
|
+
console.log(` ${DIM} --port N${RESET} ${GRAY}포트 지정 (기본 27888)${RESET}`);
|
|
1571
|
+
console.log(` ${WHITE_BRIGHT}tfx hub stop${RESET} ${GRAY}허브 중지${RESET}`);
|
|
1572
|
+
console.log(` ${WHITE_BRIGHT}tfx hub status${RESET} ${GRAY}상태 확인${RESET}\n`);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// ── 메인 ──
|
|
1577
|
+
|
|
1578
|
+
const cmd = process.argv[2] || "help";
|
|
1579
|
+
|
|
1580
|
+
switch (cmd) {
|
|
1581
|
+
case "setup": cmdSetup(); break;
|
|
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
|
+
}
|
|
1588
|
+
case "update": cmdUpdate(); break;
|
|
1589
|
+
case "list": case "ls": cmdList(); break;
|
|
1349
1590
|
case "hub": await cmdHub(); break;
|
|
1350
|
-
case "
|
|
1351
|
-
const { pathToFileURL } = await import("node:url");
|
|
1352
|
-
const { cmdTeam } = await import(pathToFileURL(join(PKG_ROOT, "hub", "team", "cli.mjs")).href);
|
|
1353
|
-
await cmdTeam();
|
|
1354
|
-
break;
|
|
1355
|
-
}
|
|
1356
|
-
case "codex-team":
|
|
1357
|
-
await cmdCodexTeam();
|
|
1358
|
-
break;
|
|
1359
|
-
case "notion-read": case "nr": {
|
|
1360
|
-
const scriptPath = join(PKG_ROOT, "scripts", "notion-read.mjs");
|
|
1361
|
-
const nrArgs = process.argv.slice(3).map(a => `"${a}"`).join(" ");
|
|
1362
|
-
try {
|
|
1363
|
-
execSync(`"${process.execPath}" "${scriptPath}" ${nrArgs}`, { stdio: "inherit", timeout: 660000 });
|
|
1364
|
-
} catch (e) { process.exit(e.status || 1); }
|
|
1365
|
-
break;
|
|
1366
|
-
}
|
|
1367
|
-
case "version": case "--version": case "-v": cmdVersion(); break;
|
|
1368
|
-
case "help": case "--help": case "-h": cmdHelp(); break;
|
|
1369
|
-
default:
|
|
1370
|
-
console.error(`알 수 없는 명령: ${cmd}`);
|
|
1371
|
-
cmdHelp();
|
|
1372
|
-
process.exit(1);
|
|
1373
|
-
}
|
|
1591
|
+
case "multi": {
|
|
1592
|
+
const { pathToFileURL } = await import("node:url");
|
|
1593
|
+
const { cmdTeam } = await import(pathToFileURL(join(PKG_ROOT, "hub", "team", "cli.mjs")).href);
|
|
1594
|
+
await cmdTeam();
|
|
1595
|
+
break;
|
|
1596
|
+
}
|
|
1597
|
+
case "codex-team":
|
|
1598
|
+
await cmdCodexTeam();
|
|
1599
|
+
break;
|
|
1600
|
+
case "notion-read": case "nr": {
|
|
1601
|
+
const scriptPath = join(PKG_ROOT, "scripts", "notion-read.mjs");
|
|
1602
|
+
const nrArgs = process.argv.slice(3).map(a => `"${a}"`).join(" ");
|
|
1603
|
+
try {
|
|
1604
|
+
execSync(`"${process.execPath}" "${scriptPath}" ${nrArgs}`, { stdio: "inherit", timeout: 660000 });
|
|
1605
|
+
} catch (e) { process.exit(e.status || 1); }
|
|
1606
|
+
break;
|
|
1607
|
+
}
|
|
1608
|
+
case "version": case "--version": case "-v": cmdVersion(); break;
|
|
1609
|
+
case "help": case "--help": case "-h": cmdHelp(); break;
|
|
1610
|
+
default:
|
|
1611
|
+
console.error(`알 수 없는 명령: ${cmd}`);
|
|
1612
|
+
cmdHelp();
|
|
1613
|
+
process.exit(1);
|
|
1614
|
+
}
|