triflux 8.2.2 → 8.3.0
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/.claude-plugin/plugin.json +1 -1
- package/bin/tfx-doctor-tui.mjs +7 -0
- package/bin/tfx-profile.mjs +7 -0
- package/bin/tfx-setup-tui.mjs +7 -0
- package/bin/triflux.mjs +4 -4
- package/hub/intent.mjs +7 -7
- package/hub/lib/process-utils.mjs +108 -38
- package/hub/team/tui.mjs +4 -0
- package/hub/workers/delegator-mcp.mjs +18 -18
- package/package.json +6 -2
- package/scripts/setup.mjs +4 -33
- package/scripts/tfx-route.sh +57 -57
- package/skills/tfx-auto-codex/SKILL.md +1 -1
- package/skills/tfx-codex/SKILL.md +2 -2
- package/skills/tfx-doctor/SKILL.md +161 -94
- package/skills/tfx-hub/SKILL.md +1 -1
- package/skills/tfx-profile/SKILL.md +149 -0
- package/skills/tfx-setup/SKILL.md +160 -101
- package/tui/codex-profile.mjs +402 -0
- package/tui/core.mjs +236 -0
- package/tui/doctor.mjs +327 -0
- package/tui/setup.mjs +362 -0
package/tui/setup.mjs
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// tui/setup.mjs — Interactive triflux setup wizard TUI
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, readdirSync } from "node:fs";
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import {
|
|
9
|
+
clear, box, table, divider, label, ok, warn, fail, info,
|
|
10
|
+
select, confirm, spinner, sleep,
|
|
11
|
+
RESET, DIM, BOLD, CYAN, AMBER, GREEN, RED, YELLOW, WHITE, GRAY,
|
|
12
|
+
onExit, showCursor,
|
|
13
|
+
} from "./core.mjs";
|
|
14
|
+
|
|
15
|
+
const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
16
|
+
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
17
|
+
const CODEX_DIR = join(homedir(), ".codex");
|
|
18
|
+
const SETTINGS_PATH = join(CLAUDE_DIR, "settings.json");
|
|
19
|
+
|
|
20
|
+
// ── Step Definitions ──
|
|
21
|
+
|
|
22
|
+
const STEPS = [
|
|
23
|
+
{ id: "sync", name: "파일 동기화", desc: "스크립트/HUD/스킬을 ~/.claude/에 배포" },
|
|
24
|
+
{ id: "hud", name: "HUD 설정", desc: "settings.json에 statusLine 등록" },
|
|
25
|
+
{ id: "profiles", name: "Codex 프로파일", desc: "필수 프로파일 생성/확인" },
|
|
26
|
+
{ id: "cli", name: "CLI 진단", desc: "Codex/Gemini/Claude CLI 확인" },
|
|
27
|
+
{ id: "mcp", name: "MCP 서버 확인", desc: "MCP 서버 인벤토리 점검" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// ── Step Implementations ──
|
|
31
|
+
|
|
32
|
+
function stepSync() {
|
|
33
|
+
try {
|
|
34
|
+
const out = execFileSync(process.execPath,
|
|
35
|
+
[join(PKG_ROOT, "bin", "triflux.mjs"), "setup", "--json"],
|
|
36
|
+
{ timeout: 30000, encoding: "utf8", windowsHide: true }
|
|
37
|
+
);
|
|
38
|
+
return { ok: true, detail: "파일 동기화 완료" };
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return { ok: false, detail: e.message };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function stepHud() {
|
|
45
|
+
try {
|
|
46
|
+
if (!existsSync(SETTINGS_PATH)) {
|
|
47
|
+
return { ok: false, detail: "settings.json 미존재", action: "create" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const raw = readFileSync(SETTINGS_PATH, "utf8");
|
|
51
|
+
let settings;
|
|
52
|
+
try { settings = JSON.parse(raw); } catch {
|
|
53
|
+
return { ok: false, detail: "settings.json 파싱 실패", action: "fix" };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const hudScript = join(CLAUDE_DIR, "hud", "hud-qos-status.mjs");
|
|
57
|
+
const nodePath = process.execPath;
|
|
58
|
+
|
|
59
|
+
// Check if statusLine already configured correctly
|
|
60
|
+
if (settings.statusLine?.command?.includes("hud-qos-status")) {
|
|
61
|
+
return { ok: true, detail: "statusLine 이미 설정됨", current: settings.statusLine };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
detail: settings.statusLine ? "statusLine이 다른 HUD를 가리킴" : "statusLine 미설정",
|
|
67
|
+
action: "configure",
|
|
68
|
+
current: settings.statusLine || null,
|
|
69
|
+
target: {
|
|
70
|
+
type: "command",
|
|
71
|
+
command: `"${nodePath}" "${hudScript}"`,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return { ok: false, detail: e.message };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stepProfiles() {
|
|
80
|
+
const configPath = join(CODEX_DIR, "config.toml");
|
|
81
|
+
if (!existsSync(configPath)) {
|
|
82
|
+
return { ok: false, detail: "config.toml 미존재", action: "skip" };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const content = readFileSync(configPath, "utf8");
|
|
86
|
+
const required = ["codex53_high", "codex53_xhigh", "spark53_low"];
|
|
87
|
+
const missing = required.filter((name) => {
|
|
88
|
+
const re = new RegExp(`^\\[profiles\\.${name}\\]`, "m");
|
|
89
|
+
return !re.test(content);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (missing.length === 0) {
|
|
93
|
+
return { ok: true, detail: `필수 프로파일 ${required.length}개 확인됨` };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { ok: false, detail: `누락: ${missing.join(", ")}`, missing, action: "create" };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function stepCli() {
|
|
100
|
+
const results = [];
|
|
101
|
+
for (const [name, installCmd] of [
|
|
102
|
+
["codex", "npm i -g @openai/codex"],
|
|
103
|
+
["gemini", "npm i -g @google/gemini-cli"],
|
|
104
|
+
["claude", "Claude Code 설치"],
|
|
105
|
+
]) {
|
|
106
|
+
try {
|
|
107
|
+
execFileSync(process.platform === "win32" ? "where" : "which", [name], {
|
|
108
|
+
timeout: 5000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"],
|
|
109
|
+
});
|
|
110
|
+
results.push({ name, found: true });
|
|
111
|
+
} catch {
|
|
112
|
+
results.push({ name, found: false, install: installCmd });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const allFound = results.every((r) => r.found);
|
|
116
|
+
return { ok: allFound, results, detail: allFound ? "모든 CLI 확인됨" : "일부 CLI 미설치" };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function stepMcp() {
|
|
120
|
+
const inventoryPath = join(CLAUDE_DIR, "cache", "mcp-inventory.json");
|
|
121
|
+
if (!existsSync(inventoryPath)) {
|
|
122
|
+
return { ok: false, detail: "MCP 인벤토리 미존재", action: "rebuild" };
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const inventory = JSON.parse(readFileSync(inventoryPath, "utf8"));
|
|
126
|
+
const count = Object.keys(inventory.servers || inventory).length;
|
|
127
|
+
return { ok: true, detail: `${count}개 MCP 서버 등록됨` };
|
|
128
|
+
} catch {
|
|
129
|
+
return { ok: false, detail: "MCP 인벤토리 파싱 실패", action: "rebuild" };
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const STEP_RUNNERS = { sync: stepSync, hud: stepHud, profiles: stepProfiles, cli: stepCli, mcp: stepMcp };
|
|
134
|
+
|
|
135
|
+
// ── UI ──
|
|
136
|
+
|
|
137
|
+
function showStepResult(step, result) {
|
|
138
|
+
if (result.ok) {
|
|
139
|
+
ok(`${step.name}: ${result.detail}`);
|
|
140
|
+
} else {
|
|
141
|
+
warn(`${step.name}: ${result.detail}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function runWizard() {
|
|
146
|
+
console.log();
|
|
147
|
+
info(`${STEPS.length}개 단계를 순서대로 실행합니다.`);
|
|
148
|
+
console.log();
|
|
149
|
+
|
|
150
|
+
const results = {};
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < STEPS.length; i++) {
|
|
153
|
+
const step = STEPS[i];
|
|
154
|
+
const progress = `${DIM}[${i + 1}/${STEPS.length}]${RESET}`;
|
|
155
|
+
|
|
156
|
+
console.log(` ${progress} ${BOLD}${step.name}${RESET} ${DIM}— ${step.desc}${RESET}`);
|
|
157
|
+
|
|
158
|
+
const spin = spinner(`${step.name} 실행 중...`);
|
|
159
|
+
const result = STEP_RUNNERS[step.id]();
|
|
160
|
+
spin.stop();
|
|
161
|
+
|
|
162
|
+
showStepResult(step, result);
|
|
163
|
+
results[step.id] = result;
|
|
164
|
+
|
|
165
|
+
// Handle fixable issues
|
|
166
|
+
if (!result.ok && result.action) {
|
|
167
|
+
await handleStepFix(step, result);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return results;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function handleStepFix(step, result) {
|
|
177
|
+
switch (step.id) {
|
|
178
|
+
case "hud": {
|
|
179
|
+
if (result.action === "configure") {
|
|
180
|
+
if (result.current) {
|
|
181
|
+
warn(`현재 statusLine: ${JSON.stringify(result.current)}`);
|
|
182
|
+
if (!(await confirm("triflux HUD로 덮어쓰시겠습니까?", false))) return;
|
|
183
|
+
} else {
|
|
184
|
+
if (!(await confirm("statusLine을 설정하시겠습니까?"))) return;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const raw = readFileSync(SETTINGS_PATH, "utf8");
|
|
188
|
+
const settings = JSON.parse(raw);
|
|
189
|
+
settings.statusLine = result.target;
|
|
190
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2), "utf8");
|
|
191
|
+
ok("statusLine 설정 완료");
|
|
192
|
+
} catch (e) {
|
|
193
|
+
fail(`설정 실패: ${e.message}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
case "profiles": {
|
|
200
|
+
if (result.action === "create" && result.missing) {
|
|
201
|
+
if (await confirm(`누락된 프로파일 ${result.missing.length}개를 생성하시겠습니까?`)) {
|
|
202
|
+
try {
|
|
203
|
+
execFileSync(process.execPath,
|
|
204
|
+
[join(PKG_ROOT, "bin", "triflux.mjs"), "setup"],
|
|
205
|
+
{ timeout: 15000, stdio: "ignore", windowsHide: true }
|
|
206
|
+
);
|
|
207
|
+
ok("프로파일 생성 완료");
|
|
208
|
+
} catch {
|
|
209
|
+
warn("프로파일 생성 실패 — triflux setup으로 재시도");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case "mcp": {
|
|
217
|
+
if (result.action === "rebuild") {
|
|
218
|
+
if (await confirm("MCP 인벤토리를 재생성하시겠습니까?")) {
|
|
219
|
+
const mcpCheck = join(PKG_ROOT, "scripts", "mcp-check.mjs");
|
|
220
|
+
if (existsSync(mcpCheck)) {
|
|
221
|
+
try {
|
|
222
|
+
execFileSync(process.execPath, [mcpCheck], { timeout: 15000, stdio: "ignore", windowsHide: true });
|
|
223
|
+
ok("MCP 인벤토리 재생성 완료");
|
|
224
|
+
} catch {
|
|
225
|
+
warn("MCP 인벤토리 재생성 실패 — 다음 세션에서 자동 재시도");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function runSelective() {
|
|
236
|
+
const options = STEPS.map((s) => ({
|
|
237
|
+
label: s.name,
|
|
238
|
+
hint: s.desc,
|
|
239
|
+
}));
|
|
240
|
+
|
|
241
|
+
const picked = await select("실행할 단계", options);
|
|
242
|
+
if (!picked) return;
|
|
243
|
+
|
|
244
|
+
const step = STEPS[picked.index];
|
|
245
|
+
console.log();
|
|
246
|
+
const spin = spinner(`${step.name} 실행 중...`);
|
|
247
|
+
const result = STEP_RUNNERS[step.id]();
|
|
248
|
+
spin.stop();
|
|
249
|
+
|
|
250
|
+
showStepResult(step, result);
|
|
251
|
+
|
|
252
|
+
if (!result.ok && result.action) {
|
|
253
|
+
await handleStepFix(step, result);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// CLI step has extra detail
|
|
257
|
+
if (step.id === "cli" && result.results) {
|
|
258
|
+
console.log();
|
|
259
|
+
for (const r of result.results) {
|
|
260
|
+
if (r.found) ok(`${r.name}: 설치됨`);
|
|
261
|
+
else warn(`${r.name}: 미설치 → ${DIM}${r.install}${RESET}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function showSummary(results) {
|
|
267
|
+
console.log();
|
|
268
|
+
divider(46);
|
|
269
|
+
box("Setup 완료", 46);
|
|
270
|
+
console.log();
|
|
271
|
+
|
|
272
|
+
const headers = ["항목", "상태"];
|
|
273
|
+
const rows = STEPS.map((s) => {
|
|
274
|
+
const r = results[s.id];
|
|
275
|
+
if (!r) return [s.name, `${GRAY}건너뜀${RESET}`];
|
|
276
|
+
return [
|
|
277
|
+
s.name,
|
|
278
|
+
r.ok ? `${GREEN}✓ 정상${RESET}` : `${YELLOW}⚠ ${r.detail}${RESET}`,
|
|
279
|
+
];
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
table(headers, rows);
|
|
283
|
+
|
|
284
|
+
const issues = Object.values(results).filter((r) => r && !r.ok).length;
|
|
285
|
+
console.log();
|
|
286
|
+
if (issues === 0) {
|
|
287
|
+
ok(`${BOLD}모든 항목 정상${RESET} — 세션을 재시작하면 적용됩니다`);
|
|
288
|
+
} else {
|
|
289
|
+
warn(`${issues}개 항목에 주의가 필요합니다`);
|
|
290
|
+
info("/tfx-doctor --fix 로 자동 수정을 시도하세요");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Main Menu ──
|
|
295
|
+
|
|
296
|
+
const MENU = [
|
|
297
|
+
{ label: "전체 설정 (Full Setup)", hint: "5단계 순서 실행" },
|
|
298
|
+
{ label: "단계별 선택 (Selective)", hint: "특정 단계만 실행" },
|
|
299
|
+
{ label: "현재 상태 확인 (Status)", hint: "설정 없이 진단만" },
|
|
300
|
+
{ label: "종료", hint: "Ctrl+C" },
|
|
301
|
+
];
|
|
302
|
+
|
|
303
|
+
async function main() {
|
|
304
|
+
onExit(() => {});
|
|
305
|
+
clear();
|
|
306
|
+
|
|
307
|
+
while (true) {
|
|
308
|
+
box("triflux Setup Wizard", 46);
|
|
309
|
+
console.log();
|
|
310
|
+
|
|
311
|
+
const choice = await select("작업 선택", MENU);
|
|
312
|
+
if (!choice || choice.index === 3) {
|
|
313
|
+
console.log();
|
|
314
|
+
info("종료합니다.");
|
|
315
|
+
showCursor();
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
console.log();
|
|
320
|
+
|
|
321
|
+
switch (choice.index) {
|
|
322
|
+
case 0: {
|
|
323
|
+
const results = await runWizard();
|
|
324
|
+
showSummary(results);
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
case 1: {
|
|
329
|
+
await runSelective();
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
case 2: {
|
|
334
|
+
info("현재 상태를 확인합니다...");
|
|
335
|
+
console.log();
|
|
336
|
+
const results = {};
|
|
337
|
+
for (const step of STEPS) {
|
|
338
|
+
if (step.id === "sync") continue; // sync는 상태 확인 불가
|
|
339
|
+
const result = STEP_RUNNERS[step.id]();
|
|
340
|
+
showStepResult(step, result);
|
|
341
|
+
results[step.id] = result;
|
|
342
|
+
if (step.id === "cli" && result.results) {
|
|
343
|
+
for (const r of result.results) {
|
|
344
|
+
if (r.found) ok(` ${r.name}: 설치됨`);
|
|
345
|
+
else warn(` ${r.name}: 미설치`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
console.log();
|
|
354
|
+
divider(46);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
main().catch((e) => {
|
|
359
|
+
showCursor();
|
|
360
|
+
console.error(e);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
});
|