triflux 8.2.3 → 8.3.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.
Files changed (49) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +209 -97
  3. package/bin/tfx-doctor-tui.mjs +7 -0
  4. package/bin/tfx-profile.mjs +7 -0
  5. package/bin/tfx-setup-tui.mjs +7 -0
  6. package/bin/triflux.mjs +14 -4
  7. package/hub/intent.mjs +7 -7
  8. package/hub/team/tui.mjs +4 -0
  9. package/hub/workers/delegator-mcp.mjs +18 -18
  10. package/package.json +6 -2
  11. package/scripts/setup.mjs +4 -33
  12. package/scripts/tfx-route.sh +57 -57
  13. package/skills/.omc/state/agent-replay-8f0e10a9-9693-4410-96f5-a6b07e8ed995.jsonl +1 -0
  14. package/skills/.omc/state/idle-notif-cooldown.json +3 -0
  15. package/skills/.omc/state/last-tool-error.json +7 -0
  16. package/skills/.omc/state/subagent-tracking.json +7 -0
  17. package/skills/tfx-analysis/SKILL.md +101 -0
  18. package/skills/tfx-auto-codex/SKILL.md +1 -1
  19. package/skills/tfx-autopilot/SKILL.md +112 -0
  20. package/skills/tfx-autoresearch/SKILL.md +1 -2
  21. package/skills/tfx-autoroute/SKILL.md +184 -0
  22. package/skills/tfx-codex/SKILL.md +2 -2
  23. package/skills/tfx-consensus/SKILL.md +112 -0
  24. package/skills/tfx-debate/SKILL.md +148 -0
  25. package/skills/tfx-deep-analysis/SKILL.md +186 -0
  26. package/skills/tfx-deep-plan/SKILL.md +113 -0
  27. package/skills/tfx-deep-qa/SKILL.md +158 -0
  28. package/skills/tfx-deep-research/SKILL.md +212 -0
  29. package/skills/tfx-deep-review/SKILL.md +91 -0
  30. package/skills/tfx-doctor/SKILL.md +161 -94
  31. package/skills/tfx-find/SKILL.md +123 -0
  32. package/skills/tfx-forge/SKILL.md +183 -0
  33. package/skills/tfx-fullcycle/SKILL.md +195 -0
  34. package/skills/tfx-hub/SKILL.md +1 -1
  35. package/skills/tfx-index/SKILL.md +174 -0
  36. package/skills/tfx-interview/SKILL.md +210 -0
  37. package/skills/tfx-panel/SKILL.md +187 -0
  38. package/skills/tfx-persist/SKILL.md +141 -0
  39. package/skills/tfx-plan/SKILL.md +53 -0
  40. package/skills/tfx-profile/SKILL.md +149 -0
  41. package/skills/tfx-prune/SKILL.md +198 -0
  42. package/skills/tfx-qa/SKILL.md +117 -0
  43. package/skills/tfx-research/SKILL.md +126 -0
  44. package/skills/tfx-review/SKILL.md +51 -0
  45. package/skills/tfx-setup/SKILL.md +160 -101
  46. package/tui/codex-profile.mjs +402 -0
  47. package/tui/core.mjs +236 -0
  48. package/tui/doctor.mjs +327 -0
  49. package/tui/setup.mjs +362 -0
@@ -1,101 +1,160 @@
1
- ---
2
- name: tfx-setup
3
- description: triflux 초기 설정 및 진단을 수행합니다.
4
- triggers:
5
- - tfx-setup
6
- argument-hint: "[doctor]"
7
- ---
8
-
9
- # tfx-setup — triflux 초기 설정 및 진단
10
-
11
- > 설치 후 최초 1회 실행 권장. HUD 설정, CLI 확인, 전체 진단을 수행합니다.
12
-
13
- ## 사용법
14
-
15
- ```
16
- /tfx-setup
17
- /tfx-setup doctor ← 진단만 실행
18
- ```
19
-
20
- ## 동작
21
-
22
- ### 기본 모드 (`/tfx-setup`)
23
-
24
- 1. **파일 동기화** `triflux setup` 실행
25
- 2. **HUD 설정** `settings.json`에 statusLine 자동 등록
26
- 3. **CLI 진단** — `triflux doctor` 실행
27
- 4. **결과 보고** 설정 상태 요약
28
-
29
- ### 진단 모드 (`/tfx-setup doctor`)
30
-
31
- `triflux doctor`만 실행하고 결과를 보고합니다.
32
-
33
- ## 실행 방법
34
-
35
- ### Step 1: 파일 동기화
36
-
37
- ```bash
38
- Bash("triflux setup")
39
- ```
40
-
41
- ### Step 2: HUD 설정 확인 및 적용
42
-
43
- `~/.claude/settings.json`을 읽어 `statusLine` 설정을 확인합니다.
44
-
45
- **statusLine이 없거나 hud-qos-status.mjs를 가리키지 않는 경우:**
46
-
47
- ```javascript
48
- // settings.json 추가할 statusLine 설정
49
- {
50
- "statusLine": {
51
- "type": "command",
52
- "command": "\"<NODE_PATH>\" \"<HOME>/.claude/hud/hud-qos-status.mjs\""
53
- }
54
- }
55
- ```
56
-
57
- - `<NODE_PATH>`: `node -e "console.log(process.execPath)"` 결과
58
- - `<HOME>`: `~` 또는 홈 디렉토리 절대 경로
59
- - Windows: 경로에 공백이 있으면 큰따옴표로 감싸기
60
- - **기존 statusLine이 있으면 덮어쓰기 전 사용자에게 확인**
61
-
62
- Read 도구로 `~/.claude/settings.json`을 읽고, Edit 도구로 statusLine을 추가/수정합니다.
63
-
64
- ### Step 3: CLI 진단
65
-
66
- ```bash
67
- Bash("triflux doctor")
68
- ```
69
-
70
- ### Step 4: 결과 보고
71
-
72
- ```markdown
73
- ## tfx-setup 완료
74
-
75
- | 항목 | 상태 |
76
- |------|------|
77
- | tfx-route.sh | ✅ v2.0 |
78
- | HUD (hud-qos-status.mjs) | ✅ v1.7 |
79
- | HUD 설정 (settings.json) | ✅ statusLine 등록됨 |
80
- | Codex CLI | ✅ / ⚠️ 미설치 (선택) |
81
- | Gemini CLI | ✅ / ⚠️ 미설치 (선택) |
82
- | 스킬 | N개 설치됨 |
83
-
84
- ### 다음 단계
85
- - Codex 미설치 시: `npm install -g @openai/codex`
86
- - Gemini 미설치 시: `npm install -g @google/gemini-cli`
87
- - 세션 재시작하면 HUD가 표시됩니다
88
- ```
89
-
90
- ## 에러 처리
91
-
92
- | 상황 | 처리 |
93
- |------|------|
94
- | `triflux: command not found` | `npm install -g triflux` 안내 |
95
- | `settings.json` 파싱 실패 | 백업 생성 후 새로 작성 |
96
- | 기존 statusLine이 다른 HUD | 사용자에게 덮어쓸지 확인 |
97
- | node.exe 경로에 공백 | 큰따옴표로 감싸기 |
98
-
99
- ## Troubleshooting
100
-
101
- 문제 발생 시 `/tfx-doctor` 실행. (`--fix` 자동 수정, `--reset` 캐시 초기화)
1
+ ---
2
+ name: tfx-setup
3
+ description: >
4
+ triflux 초기 설정 및 진단. AskUserQuestion 기반 인터랙티브 위저드로
5
+ 파일 동기화, HUD 설정, Codex 프로파일, CLI 진단, MCP 확인을 수행합니다.
6
+ Use when: setup, 설정, 설치, install, 초기화, 처음, 시작, wizard
7
+ triggers:
8
+ - tfx-setup
9
+ argument-hint: "[doctor]"
10
+ ---
11
+
12
+ # tfx-setup — triflux 초기 설정 위저드
13
+
14
+ > 설치 후 최초 1회 실행 권장. HUD 설정, CLI 확인, 전체 진단을 수행합니다.
15
+
16
+ ## 워크플로우
17
+
18
+ ### Step 1: 모드 선택 (AskUserQuestion)
19
+
20
+ ```
21
+ question: "어떤 설정 모드를 실행하시겠습니까?"
22
+ header: "모드"
23
+ options:
24
+ - label: "전체 설정 (Recommended)"
25
+ description: "5단계 순서 실행: 동기화 → HUD 프로파일 CLI MCP"
26
+ - label: "단계별 선택"
27
+ description: "필요한 단계만 골라서 실행"
28
+ - label: "현재 상태 확인"
29
+ description: "설정 없이 진단만 수행"
30
+ ```
31
+
32
+ `doctor` 인자가 있으면 바로 `triflux doctor` 실행.
33
+
34
+ ### Step 2: 전체 설정 (5단계)
35
+
36
+ 각 단계를 순서대로 실행하며 결과를 보고한다.
37
+
38
+ #### 단계 1: 파일 동기화
39
+
40
+ ```bash
41
+ Bash("triflux setup")
42
+ ```
43
+
44
+ 스크립트/HUD/스킬을 `~/.claude/`에 배포. 결과 표시.
45
+
46
+ #### 단계 2: HUD 설정
47
+
48
+ `~/.claude/settings.json`을 Read 도구로 읽어 `statusLine` 확인.
49
+
50
+ - statusLine 이미 `hud-qos-status.mjs`를 가리키면 → ✅ 표시
51
+ - statusLine이 없으면 → AskUserQuestion:
52
+ ```
53
+ question: "HUD statusLine을 설정하시겠습니까?"
54
+ header: "HUD"
55
+ options:
56
+ - label: "설정 (Recommended)"
57
+ description: "hud-qos-status.mjs를 statusLine으로 등록"
58
+ - label: "건너뛰기"
59
+ description: "나중에 수동 설정"
60
+ ```
61
+ - statusLine이 다른 값이면 → AskUserQuestion:
62
+ ```
63
+ question: "기존 statusLine을 triflux HUD로 교체하시겠습니까?"
64
+ header: "HUD"
65
+ options:
66
+ - label: "교체"
67
+ description: "기존 statusLine을 triflux HUD로 덮어씀"
68
+ - label: "유지"
69
+ description: "현재 statusLine 유지"
70
+ ```
71
+
72
+ 설정 시 Edit 도구로 settings.json 수정:
73
+ ```json
74
+ {
75
+ "statusLine": {
76
+ "type": "command",
77
+ "command": "\"<NODE_PATH>\" \"<HOME>/.claude/hud/hud-qos-status.mjs\""
78
+ }
79
+ }
80
+ ```
81
+
82
+ #### 단계 3: Codex 프로파일
83
+
84
+ `~/.codex/config.toml`을 Read 도구로 읽어 필수 프로파일 존재 여부 확인.
85
+ 필수: `codex53_high`, `codex53_xhigh`, `spark53_low`.
86
+
87
+ - 모두 존재 ✅ 표시
88
+ - 누락 있으면 → AskUserQuestion:
89
+ ```
90
+ question: "누락된 Codex 프로파일 N개를 생성하시겠습니까?"
91
+ header: "Profiles"
92
+ options:
93
+ - label: "생성 (Recommended)"
94
+ description: "누락된 프로파일을 config.toml에 추가"
95
+ - label: "건너뛰기"
96
+ description: "나중에 /tfx-profile로 관리"
97
+ ```
98
+
99
+ #### 단계 4: CLI 진단
100
+
101
+ `triflux doctor --json`을 Bash로 실행하여 CLI 존재 여부 확인.
102
+ 결과를 테이블로 표시.
103
+
104
+ #### 단계 5: MCP 서버 확인
105
+
106
+ `~/.claude/cache/mcp-inventory.json` 존재 여부 + 서버 수 확인.
107
+ 없으면 재생성 여부를 AskUserQuestion으로 확인.
108
+
109
+ ### Step 3: 단계별 선택
110
+
111
+ AskUserQuestion(multiSelect):
112
+ ```
113
+ question: "실행할 단계를 선택하세요"
114
+ header: "단계"
115
+ multiSelect: true
116
+ options:
117
+ - label: "파일 동기화"
118
+ description: "스크립트/HUD/스킬 배포"
119
+ - label: "HUD 설정"
120
+ description: "settings.json statusLine 등록"
121
+ - label: "Codex 프로파일"
122
+ description: "필수 프로파일 생성"
123
+ - label: "CLI + MCP 진단"
124
+ description: "CLI 존재 + MCP 인벤토리 확인"
125
+ ```
126
+
127
+ 선택된 단계만 순서대로 실행.
128
+
129
+ ### Step 4: 결과 요약
130
+
131
+ ```
132
+ ## tfx-setup 완료
133
+
134
+ | 항목 | 상태 |
135
+ |------|------|
136
+ | 파일 동기화 | ✅ |
137
+ | HUD 설정 | ✅ statusLine 등록됨 |
138
+ | Codex 프로파일 | ✅ 3개 확인 |
139
+ | Codex CLI | ✅ |
140
+ | Gemini CLI | ⚠ 미설치 (선택) |
141
+ | MCP 인벤토리 | ✅ N개 서버 |
142
+
143
+ ### 다음 단계
144
+ - Codex 미설치 시: `npm install -g @openai/codex`
145
+ - Gemini 미설치 시: `npm install -g @google/gemini-cli`
146
+ - 세션 재시작하면 HUD가 표시됩니다
147
+ ```
148
+
149
+ ## 에러 처리
150
+
151
+ | 상황 | 처리 |
152
+ |------|------|
153
+ | `triflux: command not found` | `npm install -g triflux` 안내 |
154
+ | `settings.json` 파싱 실패 | 백업 생성 후 새로 작성 |
155
+ | 기존 statusLine이 다른 HUD | 교체/유지 AskUserQuestion |
156
+ | node.exe 경로에 공백 | 큰따옴표로 감싸기 |
157
+
158
+ ## standalone TUI
159
+
160
+ 터미널에서 직접 실행도 가능: `node tui/setup.mjs` (arrow key 방식)
@@ -0,0 +1,402 @@
1
+ #!/usr/bin/env node
2
+ // tui/codex-profile.mjs — Interactive Codex Profile Manager
3
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import {
7
+ clear, box, table, divider, label, ok, warn, fail, info,
8
+ select, confirm, input, spinner,
9
+ RESET, DIM, BOLD, CYAN, AMBER, GREEN, RED, YELLOW, WHITE, GRAY,
10
+ onExit, showCursor,
11
+ } from "./core.mjs";
12
+
13
+ const CODEX_DIR = join(homedir(), ".codex");
14
+ const CONFIG_PATH = join(CODEX_DIR, "config.toml");
15
+
16
+ const KNOWN_MODELS = [
17
+ { label: "gpt-5.4", hint: "최신 플래그십" },
18
+ { label: "gpt-5.3-codex", hint: "코딩 특화" },
19
+ { label: "gpt-5.1-codex-mini", hint: "경량 Spark" },
20
+ { label: "o3", hint: "추론 특화" },
21
+ { label: "o4-mini", hint: "추론 경량" },
22
+ { label: "직접 입력", hint: "" },
23
+ ];
24
+
25
+ const EFFORT_LEVELS = [
26
+ { label: "low", hint: "빠른 응답, 최소 추론" },
27
+ { label: "medium", hint: "균형 잡힌 추론" },
28
+ { label: "high", hint: "깊은 추론" },
29
+ { label: "xhigh", hint: "최대 추론 (느림)" },
30
+ ];
31
+
32
+ // ── TOML Parsing ──
33
+
34
+ function readConfig() {
35
+ if (!existsSync(CONFIG_PATH)) return { raw: "", defaults: {}, profiles: [] };
36
+ const raw = readFileSync(CONFIG_PATH, "utf8");
37
+ return { raw, ...parseConfig(raw) };
38
+ }
39
+
40
+ function parseConfig(raw) {
41
+ const lines = raw.split("\n");
42
+ const defaults = {};
43
+ const profiles = [];
44
+ let currentSection = null;
45
+ let currentProfile = null;
46
+
47
+ for (const line of lines) {
48
+ const trimmed = line.trim();
49
+ if (!trimmed || trimmed.startsWith("#")) continue;
50
+
51
+ const sectionMatch = trimmed.match(/^\[(.+)\]$/);
52
+ if (sectionMatch) {
53
+ const name = sectionMatch[1];
54
+ const profileMatch = name.match(/^profiles\.(\w+)$/);
55
+ if (profileMatch) {
56
+ currentSection = "profile";
57
+ currentProfile = { name: profileMatch[1] };
58
+ profiles.push(currentProfile);
59
+ } else {
60
+ currentSection = name;
61
+ currentProfile = null;
62
+ }
63
+ continue;
64
+ }
65
+
66
+ const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
67
+ if (kvMatch) {
68
+ const [, key, rawVal] = kvMatch;
69
+ const value = rawVal.replace(/^["']|["']$/g, "").trim();
70
+ if (currentSection === "profile" && currentProfile) {
71
+ currentProfile[key] = value;
72
+ } else if (!currentSection) {
73
+ defaults[key] = value;
74
+ }
75
+ }
76
+ }
77
+
78
+ return { defaults, profiles };
79
+ }
80
+
81
+ function writeProfile(raw, profileName, props) {
82
+ const lines = raw.split("\n");
83
+ const sectionRe = new RegExp(`^\\[profiles\\.${escRe(profileName)}\\]\\s*$`);
84
+ let inSection = false;
85
+ let sectionStart = -1;
86
+ let sectionEnd = lines.length;
87
+
88
+ for (let i = 0; i < lines.length; i++) {
89
+ if (sectionRe.test(lines[i].trim())) {
90
+ inSection = true;
91
+ sectionStart = i;
92
+ continue;
93
+ }
94
+ if (inSection && lines[i].trim().startsWith("[")) {
95
+ sectionEnd = i;
96
+ break;
97
+ }
98
+ }
99
+
100
+ if (sectionStart === -1) {
101
+ // Append new profile section
102
+ const newLines = [`[profiles.${profileName}]`];
103
+ for (const [k, v] of Object.entries(props)) {
104
+ newLines.push(`${k} = "${v}"`);
105
+ }
106
+ return raw.trimEnd() + "\n" + newLines.join("\n") + "\n";
107
+ }
108
+
109
+ // Replace existing section body
110
+ const newBody = [];
111
+ for (const [k, v] of Object.entries(props)) {
112
+ newBody.push(`${k} = "${v}"`);
113
+ }
114
+ lines.splice(sectionStart + 1, sectionEnd - sectionStart - 1, ...newBody);
115
+ return lines.join("\n");
116
+ }
117
+
118
+ function deleteProfile(raw, profileName) {
119
+ const lines = raw.split("\n");
120
+ const sectionRe = new RegExp(`^\\[profiles\\.${escRe(profileName)}\\]\\s*$`);
121
+ let inSection = false;
122
+ let start = -1;
123
+ let end = lines.length;
124
+
125
+ for (let i = 0; i < lines.length; i++) {
126
+ if (sectionRe.test(lines[i].trim())) {
127
+ inSection = true;
128
+ start = i;
129
+ continue;
130
+ }
131
+ if (inSection && lines[i].trim().startsWith("[")) {
132
+ end = i;
133
+ break;
134
+ }
135
+ }
136
+
137
+ if (start === -1) return raw;
138
+ // Remove trailing blank lines too
139
+ while (end < lines.length && lines[end].trim() === "") end++;
140
+ lines.splice(start, end - start);
141
+ return lines.join("\n");
142
+ }
143
+
144
+ function setDefault(raw, key, value) {
145
+ const lines = raw.split("\n");
146
+ const keyRe = new RegExp(`^${escRe(key)}\\s*=`);
147
+
148
+ for (let i = 0; i < lines.length; i++) {
149
+ if (lines[i].trim().startsWith("[")) break; // hit first section
150
+ if (keyRe.test(lines[i].trim())) {
151
+ lines[i] = `${key} = "${value}"`;
152
+ return lines.join("\n");
153
+ }
154
+ }
155
+
156
+ // Key not found — insert before first section
157
+ for (let i = 0; i < lines.length; i++) {
158
+ if (lines[i].trim().startsWith("[")) {
159
+ lines.splice(i, 0, `${key} = "${value}"`);
160
+ return lines.join("\n");
161
+ }
162
+ }
163
+
164
+ return raw + `\n${key} = "${value}"\n`;
165
+ }
166
+
167
+ function escRe(s) {
168
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
169
+ }
170
+
171
+ // ── UI Flows ──
172
+
173
+ function showStatus(config) {
174
+ const { defaults, profiles } = config;
175
+
176
+ console.log();
177
+ label("기본 모델", `${WHITE}${defaults.model || "미설정"}${RESET}`);
178
+ label("기본 Effort", `${WHITE}${defaults.model_reasoning_effort || "미설정"}${RESET}`);
179
+ console.log();
180
+
181
+ if (profiles.length === 0) {
182
+ warn("등록된 프로파일이 없습니다.");
183
+ return;
184
+ }
185
+
186
+ const headers = ["프로파일", "모델", "Effort", "기타"];
187
+ const rows = profiles.map((p) => {
188
+ const extras = Object.entries(p)
189
+ .filter(([k]) => !["name", "model", "model_reasoning_effort"].includes(k))
190
+ .map(([k, v]) => `${k}=${v}`)
191
+ .join(", ");
192
+ return [
193
+ `${CYAN}${p.name}${RESET}`,
194
+ p.model || DIM + "inherit" + RESET,
195
+ effortColor(p.model_reasoning_effort),
196
+ extras ? `${DIM}${extras}${RESET}` : "",
197
+ ];
198
+ });
199
+
200
+ table(headers, rows);
201
+ }
202
+
203
+ function effortColor(effort) {
204
+ if (!effort) return `${DIM}inherit${RESET}`;
205
+ const colors = { low: GREEN, medium: CYAN, high: YELLOW, xhigh: RED };
206
+ return `${colors[effort] || ""}${effort}${RESET}`;
207
+ }
208
+
209
+ async function pickModel(current) {
210
+ const idx = KNOWN_MODELS.findIndex((m) => m.label === current);
211
+ const choice = await select("모델 선택", KNOWN_MODELS, { initial: Math.max(0, idx) });
212
+ if (!choice) return null;
213
+ if (choice.value.label === "직접 입력") {
214
+ return await input("모델 ID", current || "");
215
+ }
216
+ return choice.value.label;
217
+ }
218
+
219
+ async function pickEffort(current) {
220
+ const idx = EFFORT_LEVELS.findIndex((e) => e.label === current);
221
+ const choice = await select("Reasoning Effort 선택", EFFORT_LEVELS, { initial: Math.max(0, idx) });
222
+ if (!choice) return null;
223
+ return choice.value.label;
224
+ }
225
+
226
+ async function editProfile(config) {
227
+ const { profiles } = config;
228
+ if (profiles.length === 0) {
229
+ warn("편집할 프로파일이 없습니다.");
230
+ return config;
231
+ }
232
+
233
+ const options = profiles.map((p) => ({
234
+ label: p.name,
235
+ hint: `${DIM}${p.model || "inherit"} / ${p.model_reasoning_effort || "inherit"}${RESET}`,
236
+ }));
237
+
238
+ const picked = await select("편집할 프로파일", options);
239
+ if (!picked) return config;
240
+
241
+ const profile = profiles[picked.index];
242
+ console.log();
243
+ info(`현재: ${BOLD}${profile.name}${RESET} → ${profile.model} / ${profile.model_reasoning_effort}`);
244
+
245
+ const newModel = await pickModel(profile.model);
246
+ if (newModel === null) return config;
247
+
248
+ const newEffort = await pickEffort(profile.model_reasoning_effort);
249
+ if (newEffort === null) return config;
250
+
251
+ console.log();
252
+ info(`변경: ${profile.model} → ${BOLD}${newModel}${RESET}, ${profile.model_reasoning_effort} → ${BOLD}${newEffort}${RESET}`);
253
+
254
+ if (!(await confirm("저장하시겠습니까?"))) return config;
255
+
256
+ const props = { model: newModel, model_reasoning_effort: newEffort };
257
+ // Preserve extra props (like model_temperature)
258
+ for (const [k, v] of Object.entries(profile)) {
259
+ if (!["name", "model", "model_reasoning_effort"].includes(k)) props[k] = v;
260
+ }
261
+
262
+ let raw = writeProfile(config.raw, profile.name, props);
263
+ save(raw);
264
+ ok(`${profile.name} 프로파일 저장 완료`);
265
+ return readConfig();
266
+ }
267
+
268
+ async function editDefault(config) {
269
+ const { defaults } = config;
270
+ info(`현재 기본 모델: ${BOLD}${defaults.model || "미설정"}${RESET}`);
271
+ info(`현재 기본 Effort: ${BOLD}${defaults.model_reasoning_effort || "미설정"}${RESET}`);
272
+
273
+ const newModel = await pickModel(defaults.model);
274
+ if (newModel === null) return config;
275
+
276
+ const newEffort = await pickEffort(defaults.model_reasoning_effort);
277
+ if (newEffort === null) return config;
278
+
279
+ console.log();
280
+ info(`변경: ${defaults.model} → ${BOLD}${newModel}${RESET}, ${defaults.model_reasoning_effort} → ${BOLD}${newEffort}${RESET}`);
281
+
282
+ if (!(await confirm("저장하시겠습니까?"))) return config;
283
+
284
+ let raw = setDefault(config.raw, "model", newModel);
285
+ raw = setDefault(raw, "model_reasoning_effort", newEffort);
286
+ save(raw);
287
+ ok("기본 설정 저장 완료");
288
+ return readConfig();
289
+ }
290
+
291
+ async function addProfile(config) {
292
+ const name = await input("새 프로파일 이름");
293
+ if (!name) return config;
294
+
295
+ if (config.profiles.some((p) => p.name === name)) {
296
+ fail(`'${name}' 프로파일이 이미 존재합니다.`);
297
+ return config;
298
+ }
299
+
300
+ const model = await pickModel("");
301
+ if (!model) return config;
302
+
303
+ const effort = await pickEffort("");
304
+ if (!effort) return config;
305
+
306
+ console.log();
307
+ info(`추가: ${BOLD}${name}${RESET} → ${model} / ${effort}`);
308
+ if (!(await confirm("저장하시겠습니까?"))) return config;
309
+
310
+ const raw = writeProfile(config.raw, name, { model, model_reasoning_effort: effort });
311
+ save(raw);
312
+ ok(`${name} 프로파일 추가 완료`);
313
+ return readConfig();
314
+ }
315
+
316
+ async function removeProfile(config) {
317
+ const { profiles } = config;
318
+ if (profiles.length === 0) {
319
+ warn("삭제할 프로파일이 없습니다.");
320
+ return config;
321
+ }
322
+
323
+ const options = profiles.map((p) => ({ label: p.name, hint: `${p.model}` }));
324
+ const picked = await select("삭제할 프로파일", options);
325
+ if (!picked) return config;
326
+
327
+ const name = profiles[picked.index].name;
328
+ if (!(await confirm(`${RED}${name}${RESET} 프로파일을 삭제하시겠습니까?`, false))) {
329
+ return config;
330
+ }
331
+
332
+ const raw = deleteProfile(config.raw, name);
333
+ save(raw);
334
+ ok(`${name} 프로파일 삭제 완료`);
335
+ return readConfig();
336
+ }
337
+
338
+ function save(content) {
339
+ if (!existsSync(CODEX_DIR)) mkdirSync(CODEX_DIR, { recursive: true });
340
+
341
+ // Backup before write
342
+ if (existsSync(CONFIG_PATH)) {
343
+ const backupPath = CONFIG_PATH + ".bak";
344
+ copyFileSync(CONFIG_PATH, backupPath);
345
+ }
346
+
347
+ writeFileSync(CONFIG_PATH, content, "utf8");
348
+ }
349
+
350
+ // ── Main Loop ──
351
+
352
+ const MENU = [
353
+ { label: "프로파일 모델 변경", hint: "모델/effort 수정" },
354
+ { label: "기본 모델 변경", hint: "top-level default" },
355
+ { label: "프로파일 추가", hint: "새 프로파일 생성" },
356
+ { label: "프로파일 삭제", hint: "기존 프로파일 제거" },
357
+ { label: "종료", hint: "Ctrl+C" },
358
+ ];
359
+
360
+ async function main() {
361
+ onExit(() => {});
362
+ clear();
363
+
364
+ let config = readConfig();
365
+
366
+ if (!existsSync(CONFIG_PATH)) {
367
+ fail(`config.toml 미존재: ${CONFIG_PATH}`);
368
+ info("codex를 먼저 설치하거나 /tfx-setup을 실행하세요.");
369
+ process.exit(1);
370
+ }
371
+
372
+ while (true) {
373
+ box("Codex Profile Manager", 46);
374
+ showStatus(config);
375
+ console.log();
376
+
377
+ const choice = await select("작업 선택", MENU);
378
+ if (!choice || choice.index === 4) {
379
+ console.log();
380
+ info("종료합니다.");
381
+ showCursor();
382
+ break;
383
+ }
384
+
385
+ console.log();
386
+ switch (choice.index) {
387
+ case 0: config = await editProfile(config); break;
388
+ case 1: config = await editDefault(config); break;
389
+ case 2: config = await addProfile(config); break;
390
+ case 3: config = await removeProfile(config); break;
391
+ }
392
+
393
+ console.log();
394
+ divider(46);
395
+ }
396
+ }
397
+
398
+ main().catch((e) => {
399
+ showCursor();
400
+ console.error(e);
401
+ process.exit(1);
402
+ });