triflux 10.9.21 → 10.9.23

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.
Files changed (100) hide show
  1. package/.claude-plugin/marketplace.json +34 -0
  2. package/.claude-plugin/plugin.json +22 -0
  3. package/config/mcp-registry.json +29 -0
  4. package/hub/account-broker.mjs +6 -4
  5. package/hub/cli-adapter-base.mjs +14 -14
  6. package/hub/lib/env-detect.mjs +47 -20
  7. package/hub/server.mjs +17 -15
  8. package/hub/team/headless.mjs +10 -0
  9. package/hub/team/swarm-hypervisor.mjs +2 -2
  10. package/hub/workers/delegator-mcp.mjs +129 -1
  11. package/hud/constants.mjs +24 -13
  12. package/hud/renderers.mjs +2 -1
  13. package/package.json +62 -21
  14. package/scripts/__tests__/keyword-detector.test.mjs +4 -4
  15. package/scripts/__tests__/release-governance.test.mjs +148 -0
  16. package/scripts/doctor-diagnose.mjs +6 -7
  17. package/scripts/lib/cross-review-utils.mjs +2 -2
  18. package/scripts/lib/mcp-filter.mjs +12 -24
  19. package/scripts/release/bump-version.mjs +77 -0
  20. package/scripts/release/check-sync.mjs +51 -0
  21. package/scripts/release/lib.mjs +303 -0
  22. package/scripts/release/prepare.mjs +85 -0
  23. package/scripts/release/publish.mjs +87 -0
  24. package/scripts/release/verify.mjs +81 -0
  25. package/scripts/release/version-manifest.json +26 -0
  26. package/scripts/remote-spawn.mjs +3 -3
  27. package/scripts/setup.mjs +18 -15
  28. package/scripts/tfx-route.sh +64 -8
  29. package/tui/codex-profile.mjs +457 -0
  30. package/tui/core.mjs +266 -0
  31. package/tui/doctor.mjs +375 -0
  32. package/tui/gemini-profile.mjs +299 -0
  33. package/tui/monitor-data.mjs +152 -0
  34. package/tui/monitor.mjs +339 -0
  35. package/tui/setup.mjs +598 -0
  36. package/CLAUDE.md +0 -212
  37. package/references/hosts.json +0 -46
  38. package/skills/tfx-workspace/async-tests/run-tests.sh +0 -203
  39. package/skills/tfx-workspace/evals/evals.json +0 -79
  40. package/skills/tfx-workspace/iteration-1/benchmark.json +0 -524
  41. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +0 -11
  42. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +0 -25
  43. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +0 -154
  44. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +0 -5
  45. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +0 -25
  46. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +0 -126
  47. package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +0 -5
  48. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +0 -11
  49. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +0 -25
  50. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +0 -119
  51. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +0 -5
  52. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +0 -25
  53. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +0 -115
  54. package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +0 -5
  55. package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +0 -10
  56. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +0 -20
  57. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +0 -86
  58. package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +0 -5
  59. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +0 -20
  60. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +0 -81
  61. package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +0 -5
  62. package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +0 -12
  63. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +0 -30
  64. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +0 -316
  65. package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +0 -5
  66. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +0 -30
  67. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +0 -352
  68. package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +0 -5
  69. package/skills/tfx-workspace/iteration-1/review.html +0 -1325
  70. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +0 -12
  71. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +0 -30
  72. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +0 -97
  73. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +0 -5
  74. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +0 -30
  75. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +0 -94
  76. package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +0 -5
  77. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +0 -12
  78. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +0 -30
  79. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +0 -209
  80. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +0 -5
  81. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +0 -30
  82. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +0 -193
  83. package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +0 -5
  84. package/skills/tfx-workspace/iteration-2/benchmark.json +0 -144
  85. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +0 -13
  86. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +0 -35
  87. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +0 -382
  88. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +0 -5
  89. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +0 -35
  90. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +0 -333
  91. package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +0 -5
  92. package/skills/tfx-workspace/iteration-2/review.html +0 -1325
  93. package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +0 -217
  94. package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +0 -77
  95. package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +0 -65
  96. package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +0 -94
  97. package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +0 -82
  98. package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +0 -133
  99. package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +0 -426
  100. package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +0 -101
package/tui/core.mjs ADDED
@@ -0,0 +1,266 @@
1
+ #!/usr/bin/env node
2
+ // tui/core.mjs — triflux interactive TUI primitives
3
+ import readline from "node:readline";
4
+
5
+ // ── ANSI Colors (hud/colors.mjs schema) ──
6
+ export const RESET = "\x1b[0m";
7
+ export const DIM = "\x1b[2m";
8
+ export const BOLD = "\x1b[1m";
9
+ export const RED = "\x1b[31m";
10
+ export const GREEN = "\x1b[32m";
11
+ export const YELLOW = "\x1b[33m";
12
+ export const CYAN = "\x1b[36m";
13
+ export const AMBER = "\x1b[38;5;214m";
14
+ export const BLUE = "\x1b[38;5;39m";
15
+ export const WHITE = "\x1b[97m";
16
+ export const GRAY = "\x1b[38;5;245m";
17
+
18
+ // ── Screen ──
19
+
20
+ export function clear() {
21
+ process.stdout.write("\x1b[2J\x1b[H");
22
+ }
23
+
24
+ export function hideCursor() {
25
+ process.stdout.write("\x1b[?25l");
26
+ }
27
+
28
+ export function showCursor() {
29
+ process.stdout.write("\x1b[?25h");
30
+ }
31
+
32
+ function moveUp(n) {
33
+ if (n > 0) process.stdout.write(`\x1b[${n}A`);
34
+ }
35
+
36
+ function clearLine() {
37
+ process.stdout.write("\x1b[2K\r");
38
+ }
39
+
40
+ // ── Rendering ──
41
+
42
+ export function box(title, width = 50) {
43
+ const inner = width - 2;
44
+ const padded = ` ${title} `.slice(0, inner);
45
+ const left = Math.floor((inner - padded.length) / 2);
46
+ const right = inner - left - padded.length;
47
+ console.log(` ${DIM}┌${"─".repeat(inner)}┐${RESET}`);
48
+ console.log(
49
+ ` ${DIM}│${RESET}${" ".repeat(left)}${BOLD}${AMBER}${padded}${RESET}${" ".repeat(right)}${DIM}│${RESET}`,
50
+ );
51
+ console.log(` ${DIM}└${"─".repeat(inner)}┘${RESET}`);
52
+ }
53
+
54
+ export function divider(width = 50) {
55
+ console.log(` ${DIM}${"─".repeat(width - 2)}${RESET}`);
56
+ }
57
+
58
+ export function table(headers, rows, { indent = 2 } = {}) {
59
+ const pad = " ".repeat(indent);
60
+ const widths = headers.map((h, i) =>
61
+ Math.max(
62
+ stripAnsi(h).length,
63
+ ...rows.map((r) => stripAnsi(String(r[i] ?? "")).length),
64
+ ),
65
+ );
66
+
67
+ const top = widths.map((w) => "─".repeat(w + 2)).join("┬");
68
+ const mid = widths.map((w) => "─".repeat(w + 2)).join("┼");
69
+ const bot = widths.map((w) => "─".repeat(w + 2)).join("┴");
70
+
71
+ const fmtRow = (cells, color = "") =>
72
+ cells
73
+ .map((c, i) => {
74
+ const s = String(c ?? "");
75
+ const visible = stripAnsi(s).length;
76
+ return ` ${color}${s}${color ? RESET : ""}${" ".repeat(Math.max(0, widths[i] - visible))} `;
77
+ })
78
+ .join("│");
79
+
80
+ console.log(`${pad}┌${top}┐`);
81
+ console.log(`${pad}│${fmtRow(headers, BOLD)}│`);
82
+ console.log(`${pad}├${mid}┤`);
83
+ for (const row of rows) {
84
+ console.log(`${pad}│${fmtRow(row)}│`);
85
+ }
86
+ console.log(`${pad}└${bot}┘`);
87
+ }
88
+
89
+ export function ok(msg) {
90
+ console.log(` ${GREEN}✓${RESET} ${msg}`);
91
+ }
92
+ export function warn(msg) {
93
+ console.log(` ${YELLOW}⚠${RESET} ${msg}`);
94
+ }
95
+ export function fail(msg) {
96
+ console.log(` ${RED}✗${RESET} ${msg}`);
97
+ }
98
+ export function info(msg) {
99
+ console.log(` ${CYAN}ℹ${RESET} ${msg}`);
100
+ }
101
+
102
+ export function label(key, value) {
103
+ console.log(` ${DIM}${key}:${RESET} ${BOLD}${value}${RESET}`);
104
+ }
105
+
106
+ // ── Input: Arrow-key Select ──
107
+
108
+ export async function select(title, options, { initial = 0 } = {}) {
109
+ if (!process.stdin.isTTY) {
110
+ console.log(`\n ${BOLD}${title}${RESET}`);
111
+ for (let i = 0; i < options.length; i++) {
112
+ const o = typeof options[i] === "string" ? options[i] : options[i].label;
113
+ console.log(` ${DIM}${i + 1}.${RESET} ${o}`);
114
+ }
115
+ const answer = await input(
116
+ `선택 (1-${options.length})`,
117
+ String(initial + 1),
118
+ );
119
+ const idx = parseInt(answer, 10) - 1;
120
+ return idx >= 0 && idx < options.length
121
+ ? { index: idx, value: options[idx] }
122
+ : null;
123
+ }
124
+
125
+ readline.emitKeypressEvents(process.stdin);
126
+ process.stdin.setRawMode(true);
127
+ process.stdin.resume();
128
+ hideCursor();
129
+
130
+ let cursor = initial;
131
+ const total = options.length;
132
+
133
+ const getLabel = (o) => (typeof o === "string" ? o : o.label);
134
+ const getHint = (o) =>
135
+ typeof o === "object" && o.hint ? ` ${DIM}${o.hint}${RESET}` : "";
136
+
137
+ const render = (first = false) => {
138
+ if (!first) moveUp(total);
139
+ for (let i = 0; i < total; i++) {
140
+ clearLine();
141
+ const active = i === cursor;
142
+ const prefix = active ? ` ${CYAN}❯${RESET} ` : " ";
143
+ const text = active
144
+ ? `${BOLD}${getLabel(options[i])}${RESET}`
145
+ : `${DIM}${getLabel(options[i])}${RESET}`;
146
+ process.stdout.write(`${prefix}${text}${getHint(options[i])}\n`);
147
+ }
148
+ };
149
+
150
+ console.log(`\n ${BOLD}${title}${RESET}\n`);
151
+ render(true);
152
+
153
+ return new Promise((resolve) => {
154
+ const onKey = (_str, key) => {
155
+ if (!key) return;
156
+ if (key.name === "up" || key.name === "k") {
157
+ cursor = (cursor - 1 + total) % total;
158
+ render();
159
+ } else if (key.name === "down" || key.name === "j") {
160
+ cursor = (cursor + 1) % total;
161
+ render();
162
+ } else if (key.name === "return") {
163
+ cleanup();
164
+ showCursor();
165
+ resolve({ index: cursor, value: options[cursor] });
166
+ } else if (key.name === "escape" || (key.ctrl && key.name === "c")) {
167
+ cleanup();
168
+ showCursor();
169
+ resolve(null);
170
+ }
171
+ };
172
+
173
+ const cleanup = () => {
174
+ process.stdin.removeListener("keypress", onKey);
175
+ process.stdin.setRawMode(false);
176
+ process.stdin.pause();
177
+ };
178
+
179
+ process.stdin.on("keypress", onKey);
180
+ });
181
+ }
182
+
183
+ // ── Input: Confirm ──
184
+
185
+ export async function confirm(message, defaultYes = true) {
186
+ const hint = defaultYes
187
+ ? `${BOLD}Y${RESET}${DIM}/n${RESET}`
188
+ : `${DIM}y/${RESET}${BOLD}N${RESET}`;
189
+ const rl = readline.createInterface({
190
+ input: process.stdin,
191
+ output: process.stdout,
192
+ });
193
+
194
+ return new Promise((resolve) => {
195
+ rl.question(` ${CYAN}?${RESET} ${message} [${hint}] `, (answer) => {
196
+ rl.close();
197
+ const a = answer.trim().toLowerCase();
198
+ if (a === "") resolve(defaultYes);
199
+ else resolve(a === "y" || a === "yes");
200
+ });
201
+ });
202
+ }
203
+
204
+ // ── Input: Text ──
205
+
206
+ export async function input(message, defaultValue = "") {
207
+ const hint = defaultValue ? ` ${DIM}(${defaultValue})${RESET}` : "";
208
+ const rl = readline.createInterface({
209
+ input: process.stdin,
210
+ output: process.stdout,
211
+ });
212
+
213
+ return new Promise((resolve) => {
214
+ rl.question(` ${CYAN}?${RESET} ${message}${hint}: `, (answer) => {
215
+ rl.close();
216
+ resolve(answer.trim() || defaultValue);
217
+ });
218
+ });
219
+ }
220
+
221
+ // ── Spinner ──
222
+
223
+ export function spinner(message) {
224
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
225
+ let i = 0;
226
+ hideCursor();
227
+ const id = setInterval(() => {
228
+ clearLine();
229
+ process.stdout.write(
230
+ ` ${CYAN}${frames[i++ % frames.length]}${RESET} ${message}`,
231
+ );
232
+ }, 80);
233
+
234
+ return {
235
+ stop(finalMsg) {
236
+ clearInterval(id);
237
+ clearLine();
238
+ if (finalMsg) process.stdout.write(`${finalMsg}\n`);
239
+ showCursor();
240
+ },
241
+ update(msg) {
242
+ message = msg;
243
+ },
244
+ };
245
+ }
246
+
247
+ // ── Utils ──
248
+
249
+ export function stripAnsi(str) {
250
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
251
+ }
252
+
253
+ export function sleep(ms) {
254
+ return new Promise((r) => setTimeout(r, ms));
255
+ }
256
+
257
+ // graceful exit
258
+ export function onExit(fn) {
259
+ const handler = () => {
260
+ showCursor();
261
+ fn?.();
262
+ process.exit(0);
263
+ };
264
+ process.on("SIGINT", handler);
265
+ process.on("SIGTERM", handler);
266
+ }
package/tui/doctor.mjs ADDED
@@ -0,0 +1,375 @@
1
+ #!/usr/bin/env node
2
+ // tui/doctor.mjs — Interactive triflux doctor TUI
3
+ import { execFileSync } from "node:child_process";
4
+ import { existsSync, readdirSync, readFileSync, unlinkSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import {
9
+ BOLD,
10
+ box,
11
+ clear,
12
+ confirm,
13
+ DIM,
14
+ divider,
15
+ fail,
16
+ GRAY,
17
+ GREEN,
18
+ info,
19
+ label,
20
+ ok,
21
+ onExit,
22
+ RED,
23
+ RESET,
24
+ select,
25
+ showCursor,
26
+ spinner,
27
+ table,
28
+ warn,
29
+ YELLOW,
30
+ } from "./core.mjs";
31
+
32
+ const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
33
+ const CLAUDE_DIR = join(homedir(), ".claude");
34
+ const CACHE_DIR = join(CLAUDE_DIR, "cache");
35
+
36
+ const CACHE_FILES = [
37
+ { name: "claude-usage-cache.json", desc: "Claude 사용량" },
38
+ { name: ".claude-refresh-lock", desc: "리프레시 락" },
39
+ { name: "codex-rate-limits-cache.json", desc: "Codex 레이트 리밋" },
40
+ { name: "gemini-quota-cache.json", desc: "Gemini 쿼터" },
41
+ { name: "gemini-project-id.json", desc: "Gemini 프로젝트 ID" },
42
+ { name: "gemini-session-cache.json", desc: "Gemini 세션" },
43
+ { name: "gemini-rpm-tracker.json", desc: "Gemini RPM" },
44
+ { name: "sv-accumulator.json", desc: "절약량 누적" },
45
+ { name: "mcp-inventory.json", desc: "MCP 인벤토리" },
46
+ { name: "cli-issues.jsonl", desc: "CLI 이슈 로그" },
47
+ { name: "triflux-update-check.json", desc: "업데이트 체크" },
48
+ { name: "tfx-preflight.json", desc: "Preflight 캐시 (CLI/Hub 가용성)" },
49
+ ];
50
+
51
+ // ── Run triflux doctor --json ──
52
+
53
+ function runDoctor(mode = "check") {
54
+ const args = [join(PKG_ROOT, "bin", "triflux.mjs"), "doctor", "--json"];
55
+ if (mode === "fix") args.push("--fix");
56
+
57
+ try {
58
+ const out = execFileSync(process.execPath, args, {
59
+ timeout: 30000,
60
+ encoding: "utf8",
61
+ windowsHide: true,
62
+ });
63
+ // Extract JSON from output (may have ANSI/text before it)
64
+ const jsonMatch = out.match(/\{[\s\S]*\}$/m);
65
+ if (jsonMatch) return JSON.parse(jsonMatch[0]);
66
+ } catch (e) {
67
+ if (e.stdout) {
68
+ const jsonMatch = e.stdout.match(/\{[\s\S]*\}$/m);
69
+ if (jsonMatch) return JSON.parse(jsonMatch[0]);
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+
75
+ // ── Display Results ──
76
+
77
+ function statusIcon(status) {
78
+ const map = {
79
+ ok: `${GREEN}✓${RESET}`,
80
+ missing: `${RED}✗${RESET}`,
81
+ partial: `${YELLOW}⚠${RESET}`,
82
+ warning: `${YELLOW}⚠${RESET}`,
83
+ optional_missing: `${GRAY}○${RESET}`,
84
+ };
85
+ return map[status] || `${GRAY}?${RESET}`;
86
+ }
87
+
88
+ function showReport(report) {
89
+ if (!report) {
90
+ fail("진단 결과를 가져올 수 없습니다.");
91
+ return;
92
+ }
93
+
94
+ console.log();
95
+ const statusColor =
96
+ report.issue_count === 0 ? GREEN : report.issue_count <= 2 ? YELLOW : RED;
97
+ label(
98
+ "상태",
99
+ `${statusColor}${report.issue_count === 0 ? "정상" : `${report.issue_count}개 이슈`}${RESET}`,
100
+ );
101
+ label("모드", report.mode);
102
+ console.log();
103
+
104
+ const headers = ["항목", "상태", "비고"];
105
+ const rows = (report.checks || []).map((c) => {
106
+ let note = "";
107
+ if (c.version) note = `v${c.version}`;
108
+ if (c.missing_profiles?.length)
109
+ note = `누락: ${c.missing_profiles.join(", ")}`;
110
+ if (c.fix) note += note ? ` → ${c.fix}` : c.fix;
111
+ if (c.path && !c.fix) note = c.path;
112
+
113
+ const icon =
114
+ c.status === "ok"
115
+ ? statusIcon("ok")
116
+ : c.optional
117
+ ? statusIcon("optional_missing")
118
+ : statusIcon(c.status);
119
+
120
+ return [
121
+ `${icon} ${c.name}`,
122
+ c.status === "ok" ? `${GREEN}정상${RESET}` : `${RED}${c.status}${RESET}`,
123
+ note ? `${DIM}${note}${RESET}` : "",
124
+ ];
125
+ });
126
+
127
+ if (rows.length > 0) table(headers, rows);
128
+
129
+ // Actions (from fix/reset mode)
130
+ if (report.actions?.length > 0) {
131
+ console.log();
132
+ info(`수행된 작업: ${report.actions.length}개`);
133
+ for (const action of report.actions) {
134
+ const icon =
135
+ action.status === "ok" ? `${GREEN}✓${RESET}` : `${RED}✗${RESET}`;
136
+ console.log(
137
+ ` ${icon} ${action.type}: ${action.name || action.path || ""}`,
138
+ );
139
+ }
140
+ }
141
+ }
142
+
143
+ // ── Cache Management ──
144
+
145
+ function getCacheStatus() {
146
+ const results = [];
147
+ for (const { name, desc } of CACHE_FILES) {
148
+ const fp = join(CACHE_DIR, name);
149
+ if (existsSync(fp)) {
150
+ let size = 0;
151
+ try {
152
+ size = readFileSync(fp).length;
153
+ } catch {}
154
+ let hasError = false;
155
+ try {
156
+ const parsed = JSON.parse(readFileSync(fp, "utf8"));
157
+ hasError = !!parsed.error;
158
+ } catch {
159
+ hasError = true;
160
+ }
161
+ results.push({ name, desc, exists: true, size, hasError });
162
+ } else {
163
+ results.push({ name, desc, exists: false, size: 0, hasError: false });
164
+ }
165
+ }
166
+ return results;
167
+ }
168
+
169
+ function showCacheStatus() {
170
+ const caches = getCacheStatus();
171
+ const existing = caches.filter((c) => c.exists);
172
+
173
+ if (existing.length === 0) {
174
+ info("캐시 파일 없음 (깨끗한 상태)");
175
+ return;
176
+ }
177
+
178
+ console.log();
179
+ const headers = ["캐시", "크기", "상태"];
180
+ const rows = existing.map((c) => [
181
+ c.desc,
182
+ c.size < 1024 ? `${c.size}B` : `${(c.size / 1024).toFixed(1)}KB`,
183
+ c.hasError ? `${RED}에러${RESET}` : `${GREEN}정상${RESET}`,
184
+ ]);
185
+ table(headers, rows);
186
+ }
187
+
188
+ async function selectiveReset() {
189
+ const caches = getCacheStatus().filter((c) => c.exists);
190
+ if (caches.length === 0) {
191
+ info("삭제할 캐시 파일이 없습니다.");
192
+ return;
193
+ }
194
+
195
+ const options = [
196
+ { label: "전체 삭제", hint: `${caches.length}개 파일` },
197
+ { label: "에러 캐시만 삭제", hint: "손상된 파일만" },
198
+ { label: "선택 삭제", hint: "하나씩 선택" },
199
+ { label: "취소", hint: "" },
200
+ ];
201
+
202
+ const choice = await select("삭제 방식", options);
203
+ if (!choice || choice.index === 3) return;
204
+
205
+ let targets = [];
206
+ if (choice.index === 0) {
207
+ targets = caches;
208
+ } else if (choice.index === 1) {
209
+ targets = caches.filter((c) => c.hasError);
210
+ if (targets.length === 0) {
211
+ info("에러 상태의 캐시가 없습니다.");
212
+ return;
213
+ }
214
+ } else {
215
+ for (const c of caches) {
216
+ const del = await confirm(`${c.desc} (${c.name}) 삭제?`, c.hasError);
217
+ if (del) targets.push(c);
218
+ }
219
+ }
220
+
221
+ if (targets.length === 0) return;
222
+
223
+ if (!(await confirm(`${targets.length}개 캐시 파일을 삭제하시겠습니까?`)))
224
+ return;
225
+
226
+ let deleted = 0;
227
+ for (const c of targets) {
228
+ try {
229
+ unlinkSync(join(CACHE_DIR, c.name));
230
+ ok(`삭제: ${c.desc}`);
231
+ deleted++;
232
+ } catch (e) {
233
+ fail(`삭제 실패: ${c.desc} — ${e.message}`);
234
+ }
235
+ }
236
+
237
+ ok(`${BOLD}${deleted}개${RESET} 캐시 파일 삭제 완료`);
238
+ }
239
+
240
+ // ── Orphan Teams ──
241
+
242
+ async function checkOrphanTeams() {
243
+ const teamsDir = join(CLAUDE_DIR, "teams");
244
+ if (!existsSync(teamsDir)) {
245
+ info("teams 디렉토리 없음");
246
+ return;
247
+ }
248
+
249
+ const entries = readdirSync(teamsDir).filter((e) => !e.startsWith("."));
250
+ if (entries.length === 0) {
251
+ ok("잔존 팀 없음");
252
+ return;
253
+ }
254
+
255
+ warn(`${entries.length}개 팀 세션 발견`);
256
+ for (const e of entries) {
257
+ console.log(` ${DIM}${e}${RESET}`);
258
+ }
259
+
260
+ if (await confirm("잔존 팀 정리를 시도하시겠습니까?", false)) {
261
+ const spin = spinner("팀 정리 중...");
262
+ try {
263
+ // Delegate to triflux's cleanup
264
+ execFileSync(
265
+ process.execPath,
266
+ [join(PKG_ROOT, "bin", "triflux.mjs"), "doctor", "--fix"],
267
+ {
268
+ timeout: 30000,
269
+ stdio: "ignore",
270
+ windowsHide: true,
271
+ },
272
+ );
273
+ spin.stop();
274
+ ok("팀 정리 완료");
275
+ } catch {
276
+ spin.stop();
277
+ warn("팀 정리 실패 — 수동 삭제가 필요할 수 있습니다");
278
+ }
279
+ }
280
+ }
281
+
282
+ // ── Main Menu ──
283
+
284
+ const MENU = [
285
+ { label: "진단 (Diagnose)", hint: "읽기 전용 검사" },
286
+ { label: "수정 (Fix)", hint: "자동 수정 + 진단" },
287
+ { label: "캐시 관리 (Cache)", hint: "캐시 조회/선택 삭제" },
288
+ { label: "팀 세션 정리 (Teams)", hint: "잔존 팀 감지/정리" },
289
+ { label: "전체 초기화 (Reset)", hint: "캐시 전체 삭제 + 재생성" },
290
+ { label: "종료", hint: "Ctrl+C" },
291
+ ];
292
+
293
+ async function main() {
294
+ onExit(() => {});
295
+ clear();
296
+
297
+ while (true) {
298
+ box("triflux Doctor", 46);
299
+ console.log();
300
+
301
+ const choice = await select("작업 선택", MENU);
302
+ if (!choice || choice.index === 5) {
303
+ console.log();
304
+ info("종료합니다.");
305
+ showCursor();
306
+ break;
307
+ }
308
+
309
+ console.log();
310
+
311
+ switch (choice.index) {
312
+ case 0: {
313
+ const spin = spinner("진단 중...");
314
+ const report = runDoctor("check");
315
+ spin.stop();
316
+ showReport(report);
317
+ break;
318
+ }
319
+
320
+ case 1: {
321
+ if (!(await confirm("자동 수정을 실행하시겠습니까?"))) break;
322
+ const spin = spinner("수정 + 진단 중...");
323
+ const report = runDoctor("fix");
324
+ spin.stop();
325
+ showReport(report);
326
+ break;
327
+ }
328
+
329
+ case 2: {
330
+ showCacheStatus();
331
+ console.log();
332
+ await selectiveReset();
333
+ break;
334
+ }
335
+
336
+ case 3: {
337
+ await checkOrphanTeams();
338
+ break;
339
+ }
340
+
341
+ case 4: {
342
+ if (
343
+ !(await confirm(
344
+ `${RED}전체 캐시를 초기화${RESET}하시겠습니까?`,
345
+ false,
346
+ ))
347
+ )
348
+ break;
349
+ const spin = spinner("초기화 + 재생성 중...");
350
+ try {
351
+ execFileSync(
352
+ process.execPath,
353
+ [join(PKG_ROOT, "bin", "triflux.mjs"), "doctor", "--reset"],
354
+ { timeout: 60000, encoding: "utf8", windowsHide: true },
355
+ );
356
+ spin.stop();
357
+ ok("전체 초기화 + 재생성 완료");
358
+ } catch {
359
+ spin.stop();
360
+ warn("초기화 중 일부 실패 — triflux doctor --reset으로 재시도");
361
+ }
362
+ break;
363
+ }
364
+ }
365
+
366
+ console.log();
367
+ divider(46);
368
+ }
369
+ }
370
+
371
+ main().catch((e) => {
372
+ showCursor();
373
+ console.error(e);
374
+ process.exit(1);
375
+ });