triflux 10.9.21 → 10.9.22
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/marketplace.json +34 -0
- package/.claude-plugin/plugin.json +22 -0
- package/config/mcp-registry.json +29 -0
- package/hub/account-broker.mjs +6 -4
- package/hub/cli-adapter-base.mjs +14 -14
- package/hub/lib/env-detect.mjs +47 -20
- package/hub/server.mjs +17 -15
- package/hub/team/headless.mjs +10 -0
- package/hub/team/swarm-hypervisor.mjs +2 -2
- package/hud/constants.mjs +24 -13
- package/hud/renderers.mjs +2 -1
- package/package.json +62 -21
- package/scripts/__tests__/keyword-detector.test.mjs +4 -4
- package/scripts/__tests__/release-governance.test.mjs +148 -0
- package/scripts/doctor-diagnose.mjs +6 -7
- package/scripts/lib/cross-review-utils.mjs +2 -2
- package/scripts/lib/mcp-filter.mjs +9 -5
- package/scripts/release/bump-version.mjs +77 -0
- package/scripts/release/check-sync.mjs +51 -0
- package/scripts/release/lib.mjs +303 -0
- package/scripts/release/prepare.mjs +85 -0
- package/scripts/release/publish.mjs +87 -0
- package/scripts/release/verify.mjs +81 -0
- package/scripts/release/version-manifest.json +26 -0
- package/scripts/remote-spawn.mjs +3 -3
- package/scripts/setup.mjs +18 -15
- package/scripts/tfx-route.sh +64 -8
- package/tui/codex-profile.mjs +457 -0
- package/tui/core.mjs +266 -0
- package/tui/doctor.mjs +375 -0
- package/tui/gemini-profile.mjs +299 -0
- package/tui/monitor-data.mjs +152 -0
- package/tui/monitor.mjs +339 -0
- package/tui/setup.mjs +598 -0
- package/CLAUDE.md +0 -212
- package/references/hosts.json +0 -46
- package/skills/tfx-workspace/async-tests/run-tests.sh +0 -203
- package/skills/tfx-workspace/evals/evals.json +0 -79
- package/skills/tfx-workspace/iteration-1/benchmark.json +0 -524
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/eval_metadata.json +0 -11
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/grading.json +0 -25
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/outputs/analysis.md +0 -154
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/grading.json +0 -25
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/outputs/analysis.md +0 -126
- package/skills/tfx-workspace/iteration-1/codex-gemini-remap/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/eval_metadata.json +0 -11
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/grading.json +0 -25
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/outputs/analysis.md +0 -119
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/grading.json +0 -25
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/outputs/analysis.md +0 -115
- package/skills/tfx-workspace/iteration-1/doctor-diagnosis/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/eval_metadata.json +0 -10
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/grading.json +0 -20
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/outputs/analysis.md +0 -86
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/grading.json +0 -20
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/outputs/analysis.md +0 -81
- package/skills/tfx-workspace/iteration-1/hub-start-sequence/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/multi-team-creation/eval_metadata.json +0 -12
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/outputs/analysis.md +0 -316
- package/skills/tfx-workspace/iteration-1/multi-team-creation/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/outputs/analysis.md +0 -352
- package/skills/tfx-workspace/iteration-1/multi-team-creation/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/review.html +0 -1325
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/eval_metadata.json +0 -12
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/outputs/analysis.md +0 -97
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/outputs/analysis.md +0 -94
- package/skills/tfx-workspace/iteration-1/routing-implement-shortcut/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/eval_metadata.json +0 -12
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/outputs/analysis.md +0 -209
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/grading.json +0 -30
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/outputs/analysis.md +0 -193
- package/skills/tfx-workspace/iteration-1/routing-multi-task-triage/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-2/benchmark.json +0 -144
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/eval_metadata.json +0 -13
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/grading.json +0 -35
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/outputs/analysis.md +0 -382
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/old_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/grading.json +0 -35
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/outputs/analysis.md +0 -333
- package/skills/tfx-workspace/iteration-2/multi-team-creation-refactored/with_skill/timing.json +0 -5
- package/skills/tfx-workspace/iteration-2/review.html +0 -1325
- package/skills/tfx-workspace/skill-snapshot/tfx-auto/SKILL.md +0 -217
- package/skills/tfx-workspace/skill-snapshot/tfx-auto-codex/SKILL.md +0 -77
- package/skills/tfx-workspace/skill-snapshot/tfx-codex/SKILL.md +0 -65
- package/skills/tfx-workspace/skill-snapshot/tfx-doctor/SKILL.md +0 -94
- package/skills/tfx-workspace/skill-snapshot/tfx-gemini/SKILL.md +0 -82
- package/skills/tfx-workspace/skill-snapshot/tfx-hub/SKILL.md +0 -133
- package/skills/tfx-workspace/skill-snapshot/tfx-multi/SKILL.md +0 -426
- package/skills/tfx-workspace/skill-snapshot/tfx-setup/SKILL.md +0 -101
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// tui/codex-profile.mjs — Interactive Codex Profile Manager
|
|
3
|
+
import {
|
|
4
|
+
copyFileSync,
|
|
5
|
+
existsSync,
|
|
6
|
+
mkdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import {
|
|
13
|
+
BOLD,
|
|
14
|
+
box,
|
|
15
|
+
CYAN,
|
|
16
|
+
clear,
|
|
17
|
+
confirm,
|
|
18
|
+
DIM,
|
|
19
|
+
divider,
|
|
20
|
+
fail,
|
|
21
|
+
GREEN,
|
|
22
|
+
info,
|
|
23
|
+
input,
|
|
24
|
+
label,
|
|
25
|
+
ok,
|
|
26
|
+
onExit,
|
|
27
|
+
RED,
|
|
28
|
+
RESET,
|
|
29
|
+
select,
|
|
30
|
+
showCursor,
|
|
31
|
+
table,
|
|
32
|
+
WHITE,
|
|
33
|
+
warn,
|
|
34
|
+
YELLOW,
|
|
35
|
+
} from "./core.mjs";
|
|
36
|
+
|
|
37
|
+
const CODEX_DIR = join(homedir(), ".codex");
|
|
38
|
+
const CONFIG_PATH = join(CODEX_DIR, "config.toml");
|
|
39
|
+
|
|
40
|
+
const KNOWN_MODELS = [
|
|
41
|
+
{ label: "gpt-5.4", hint: "최신 플래그십" },
|
|
42
|
+
{ label: "gpt-5.3-codex", hint: "코딩 특화" },
|
|
43
|
+
{ label: "gpt-5.1-codex-mini", hint: "경량 Spark" },
|
|
44
|
+
{ label: "o3", hint: "추론 특화" },
|
|
45
|
+
{ label: "o4-mini", hint: "추론 경량" },
|
|
46
|
+
{ label: "직접 입력", hint: "" },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const EFFORT_LEVELS = [
|
|
50
|
+
{ label: "low", hint: "빠른 응답, 최소 추론" },
|
|
51
|
+
{ label: "medium", hint: "균형 잡힌 추론" },
|
|
52
|
+
{ label: "high", hint: "깊은 추론" },
|
|
53
|
+
{ label: "xhigh", hint: "최대 추론 (느림)" },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// ── TOML Parsing ──
|
|
57
|
+
|
|
58
|
+
function readConfig() {
|
|
59
|
+
if (!existsSync(CONFIG_PATH)) return { raw: "", defaults: {}, profiles: [] };
|
|
60
|
+
const raw = readFileSync(CONFIG_PATH, "utf8");
|
|
61
|
+
return { raw, ...parseConfig(raw) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseConfig(raw) {
|
|
65
|
+
const lines = raw.split("\n");
|
|
66
|
+
const defaults = {};
|
|
67
|
+
const profiles = [];
|
|
68
|
+
let currentSection = null;
|
|
69
|
+
let currentProfile = null;
|
|
70
|
+
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
const trimmed = line.trim();
|
|
73
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
74
|
+
|
|
75
|
+
const sectionMatch = trimmed.match(/^\[(.+)\]$/);
|
|
76
|
+
if (sectionMatch) {
|
|
77
|
+
const name = sectionMatch[1];
|
|
78
|
+
const profileMatch = name.match(/^profiles\.(\w+)$/);
|
|
79
|
+
if (profileMatch) {
|
|
80
|
+
currentSection = "profile";
|
|
81
|
+
currentProfile = { name: profileMatch[1] };
|
|
82
|
+
profiles.push(currentProfile);
|
|
83
|
+
} else {
|
|
84
|
+
currentSection = name;
|
|
85
|
+
currentProfile = null;
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const kvMatch = trimmed.match(/^(\w+)\s*=\s*(.+)$/);
|
|
91
|
+
if (kvMatch) {
|
|
92
|
+
const [, key, rawVal] = kvMatch;
|
|
93
|
+
const value = rawVal.replace(/^["']|["']$/g, "").trim();
|
|
94
|
+
if (currentSection === "profile" && currentProfile) {
|
|
95
|
+
currentProfile[key] = value;
|
|
96
|
+
} else if (!currentSection) {
|
|
97
|
+
defaults[key] = value;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { defaults, profiles };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function writeProfile(raw, profileName, props) {
|
|
106
|
+
const lines = raw.split("\n");
|
|
107
|
+
const sectionRe = new RegExp(`^\\[profiles\\.${escRe(profileName)}\\]\\s*$`);
|
|
108
|
+
let inSection = false;
|
|
109
|
+
let sectionStart = -1;
|
|
110
|
+
let sectionEnd = lines.length;
|
|
111
|
+
|
|
112
|
+
for (let i = 0; i < lines.length; i++) {
|
|
113
|
+
if (sectionRe.test(lines[i].trim())) {
|
|
114
|
+
inSection = true;
|
|
115
|
+
sectionStart = i;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (inSection && lines[i].trim().startsWith("[")) {
|
|
119
|
+
sectionEnd = i;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (sectionStart === -1) {
|
|
125
|
+
// Append new profile section
|
|
126
|
+
const newLines = [`[profiles.${profileName}]`];
|
|
127
|
+
for (const [k, v] of Object.entries(props)) {
|
|
128
|
+
newLines.push(`${k} = "${v}"`);
|
|
129
|
+
}
|
|
130
|
+
return raw.trimEnd() + "\n" + newLines.join("\n") + "\n";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Replace existing section body
|
|
134
|
+
const newBody = [];
|
|
135
|
+
for (const [k, v] of Object.entries(props)) {
|
|
136
|
+
newBody.push(`${k} = "${v}"`);
|
|
137
|
+
}
|
|
138
|
+
lines.splice(sectionStart + 1, sectionEnd - sectionStart - 1, ...newBody);
|
|
139
|
+
return lines.join("\n");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function deleteProfile(raw, profileName) {
|
|
143
|
+
const lines = raw.split("\n");
|
|
144
|
+
const sectionRe = new RegExp(`^\\[profiles\\.${escRe(profileName)}\\]\\s*$`);
|
|
145
|
+
let inSection = false;
|
|
146
|
+
let start = -1;
|
|
147
|
+
let end = lines.length;
|
|
148
|
+
|
|
149
|
+
for (let i = 0; i < lines.length; i++) {
|
|
150
|
+
if (sectionRe.test(lines[i].trim())) {
|
|
151
|
+
inSection = true;
|
|
152
|
+
start = i;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (inSection && lines[i].trim().startsWith("[")) {
|
|
156
|
+
end = i;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (start === -1) return raw;
|
|
162
|
+
// Remove trailing blank lines too
|
|
163
|
+
while (end < lines.length && lines[end].trim() === "") end++;
|
|
164
|
+
lines.splice(start, end - start);
|
|
165
|
+
return lines.join("\n");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function setDefault(raw, key, value) {
|
|
169
|
+
const lines = raw.split("\n");
|
|
170
|
+
const keyRe = new RegExp(`^${escRe(key)}\\s*=`);
|
|
171
|
+
|
|
172
|
+
for (let i = 0; i < lines.length; i++) {
|
|
173
|
+
if (lines[i].trim().startsWith("[")) break; // hit first section
|
|
174
|
+
if (keyRe.test(lines[i].trim())) {
|
|
175
|
+
lines[i] = `${key} = "${value}"`;
|
|
176
|
+
return lines.join("\n");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Key not found — insert before first section
|
|
181
|
+
for (let i = 0; i < lines.length; i++) {
|
|
182
|
+
if (lines[i].trim().startsWith("[")) {
|
|
183
|
+
lines.splice(i, 0, `${key} = "${value}"`);
|
|
184
|
+
return lines.join("\n");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return raw + `\n${key} = "${value}"\n`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function escRe(s) {
|
|
192
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── UI Flows ──
|
|
196
|
+
|
|
197
|
+
function showStatus(config) {
|
|
198
|
+
const { defaults, profiles } = config;
|
|
199
|
+
|
|
200
|
+
console.log();
|
|
201
|
+
label("기본 모델", `${WHITE}${defaults.model || "미설정"}${RESET}`);
|
|
202
|
+
label(
|
|
203
|
+
"기본 Effort",
|
|
204
|
+
`${WHITE}${defaults.model_reasoning_effort || "미설정"}${RESET}`,
|
|
205
|
+
);
|
|
206
|
+
console.log();
|
|
207
|
+
|
|
208
|
+
if (profiles.length === 0) {
|
|
209
|
+
warn("등록된 프로파일이 없습니다.");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const headers = ["프로파일", "모델", "Effort", "기타"];
|
|
214
|
+
const rows = profiles.map((p) => {
|
|
215
|
+
const extras = Object.entries(p)
|
|
216
|
+
.filter(([k]) => !["name", "model", "model_reasoning_effort"].includes(k))
|
|
217
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
218
|
+
.join(", ");
|
|
219
|
+
return [
|
|
220
|
+
`${CYAN}${p.name}${RESET}`,
|
|
221
|
+
p.model || DIM + "inherit" + RESET,
|
|
222
|
+
effortColor(p.model_reasoning_effort),
|
|
223
|
+
extras ? `${DIM}${extras}${RESET}` : "",
|
|
224
|
+
];
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
table(headers, rows);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function effortColor(effort) {
|
|
231
|
+
if (!effort) return `${DIM}inherit${RESET}`;
|
|
232
|
+
const colors = { low: GREEN, medium: CYAN, high: YELLOW, xhigh: RED };
|
|
233
|
+
return `${colors[effort] || ""}${effort}${RESET}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function pickModel(current) {
|
|
237
|
+
const idx = KNOWN_MODELS.findIndex((m) => m.label === current);
|
|
238
|
+
const choice = await select("모델 선택", KNOWN_MODELS, {
|
|
239
|
+
initial: Math.max(0, idx),
|
|
240
|
+
});
|
|
241
|
+
if (!choice) return null;
|
|
242
|
+
if (choice.value.label === "직접 입력") {
|
|
243
|
+
return await input("모델 ID", current || "");
|
|
244
|
+
}
|
|
245
|
+
return choice.value.label;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function pickEffort(current) {
|
|
249
|
+
const idx = EFFORT_LEVELS.findIndex((e) => e.label === current);
|
|
250
|
+
const choice = await select("Reasoning Effort 선택", EFFORT_LEVELS, {
|
|
251
|
+
initial: Math.max(0, idx),
|
|
252
|
+
});
|
|
253
|
+
if (!choice) return null;
|
|
254
|
+
return choice.value.label;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function editProfile(config) {
|
|
258
|
+
const { profiles } = config;
|
|
259
|
+
if (profiles.length === 0) {
|
|
260
|
+
warn("편집할 프로파일이 없습니다.");
|
|
261
|
+
return config;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const options = profiles.map((p) => ({
|
|
265
|
+
label: p.name,
|
|
266
|
+
hint: `${DIM}${p.model || "inherit"} / ${p.model_reasoning_effort || "inherit"}${RESET}`,
|
|
267
|
+
}));
|
|
268
|
+
|
|
269
|
+
const picked = await select("편집할 프로파일", options);
|
|
270
|
+
if (!picked) return config;
|
|
271
|
+
|
|
272
|
+
const profile = profiles[picked.index];
|
|
273
|
+
console.log();
|
|
274
|
+
info(
|
|
275
|
+
`현재: ${BOLD}${profile.name}${RESET} → ${profile.model} / ${profile.model_reasoning_effort}`,
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const newModel = await pickModel(profile.model);
|
|
279
|
+
if (newModel === null) return config;
|
|
280
|
+
|
|
281
|
+
const newEffort = await pickEffort(profile.model_reasoning_effort);
|
|
282
|
+
if (newEffort === null) return config;
|
|
283
|
+
|
|
284
|
+
console.log();
|
|
285
|
+
info(
|
|
286
|
+
`변경: ${profile.model} → ${BOLD}${newModel}${RESET}, ${profile.model_reasoning_effort} → ${BOLD}${newEffort}${RESET}`,
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
if (!(await confirm("저장하시겠습니까?"))) return config;
|
|
290
|
+
|
|
291
|
+
const props = { model: newModel, model_reasoning_effort: newEffort };
|
|
292
|
+
// Preserve extra props (like model_temperature)
|
|
293
|
+
for (const [k, v] of Object.entries(profile)) {
|
|
294
|
+
if (!["name", "model", "model_reasoning_effort"].includes(k)) props[k] = v;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const raw = writeProfile(config.raw, profile.name, props);
|
|
298
|
+
save(raw);
|
|
299
|
+
ok(`${profile.name} 프로파일 저장 완료`);
|
|
300
|
+
return readConfig();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function editDefault(config) {
|
|
304
|
+
const { defaults } = config;
|
|
305
|
+
info(`현재 기본 모델: ${BOLD}${defaults.model || "미설정"}${RESET}`);
|
|
306
|
+
info(
|
|
307
|
+
`현재 기본 Effort: ${BOLD}${defaults.model_reasoning_effort || "미설정"}${RESET}`,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
const newModel = await pickModel(defaults.model);
|
|
311
|
+
if (newModel === null) return config;
|
|
312
|
+
|
|
313
|
+
const newEffort = await pickEffort(defaults.model_reasoning_effort);
|
|
314
|
+
if (newEffort === null) return config;
|
|
315
|
+
|
|
316
|
+
console.log();
|
|
317
|
+
info(
|
|
318
|
+
`변경: ${defaults.model} → ${BOLD}${newModel}${RESET}, ${defaults.model_reasoning_effort} → ${BOLD}${newEffort}${RESET}`,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
if (!(await confirm("저장하시겠습니까?"))) return config;
|
|
322
|
+
|
|
323
|
+
let raw = setDefault(config.raw, "model", newModel);
|
|
324
|
+
raw = setDefault(raw, "model_reasoning_effort", newEffort);
|
|
325
|
+
save(raw);
|
|
326
|
+
ok("기본 설정 저장 완료");
|
|
327
|
+
return readConfig();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function addProfile(config) {
|
|
331
|
+
const name = await input("새 프로파일 이름");
|
|
332
|
+
if (!name) return config;
|
|
333
|
+
|
|
334
|
+
if (config.profiles.some((p) => p.name === name)) {
|
|
335
|
+
fail(`'${name}' 프로파일이 이미 존재합니다.`);
|
|
336
|
+
return config;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const model = await pickModel("");
|
|
340
|
+
if (!model) return config;
|
|
341
|
+
|
|
342
|
+
const effort = await pickEffort("");
|
|
343
|
+
if (!effort) return config;
|
|
344
|
+
|
|
345
|
+
console.log();
|
|
346
|
+
info(`추가: ${BOLD}${name}${RESET} → ${model} / ${effort}`);
|
|
347
|
+
if (!(await confirm("저장하시겠습니까?"))) return config;
|
|
348
|
+
|
|
349
|
+
const raw = writeProfile(config.raw, name, {
|
|
350
|
+
model,
|
|
351
|
+
model_reasoning_effort: effort,
|
|
352
|
+
});
|
|
353
|
+
save(raw);
|
|
354
|
+
ok(`${name} 프로파일 추가 완료`);
|
|
355
|
+
return readConfig();
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function removeProfile(config) {
|
|
359
|
+
const { profiles } = config;
|
|
360
|
+
if (profiles.length === 0) {
|
|
361
|
+
warn("삭제할 프로파일이 없습니다.");
|
|
362
|
+
return config;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const options = profiles.map((p) => ({ label: p.name, hint: `${p.model}` }));
|
|
366
|
+
const picked = await select("삭제할 프로파일", options);
|
|
367
|
+
if (!picked) return config;
|
|
368
|
+
|
|
369
|
+
const name = profiles[picked.index].name;
|
|
370
|
+
if (
|
|
371
|
+
!(await confirm(
|
|
372
|
+
`${RED}${name}${RESET} 프로파일을 삭제하시겠습니까?`,
|
|
373
|
+
false,
|
|
374
|
+
))
|
|
375
|
+
) {
|
|
376
|
+
return config;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const raw = deleteProfile(config.raw, name);
|
|
380
|
+
save(raw);
|
|
381
|
+
ok(`${name} 프로파일 삭제 완료`);
|
|
382
|
+
return readConfig();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function save(content) {
|
|
386
|
+
if (!existsSync(CODEX_DIR)) mkdirSync(CODEX_DIR, { recursive: true });
|
|
387
|
+
|
|
388
|
+
// Backup before write
|
|
389
|
+
if (existsSync(CONFIG_PATH)) {
|
|
390
|
+
const backupPath = CONFIG_PATH + ".bak";
|
|
391
|
+
copyFileSync(CONFIG_PATH, backupPath);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
writeFileSync(CONFIG_PATH, content, "utf8");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ── Main Loop ──
|
|
398
|
+
|
|
399
|
+
const MENU = [
|
|
400
|
+
{ label: "프로파일 모델 변경", hint: "모델/effort 수정" },
|
|
401
|
+
{ label: "기본 모델 변경", hint: "top-level default" },
|
|
402
|
+
{ label: "프로파일 추가", hint: "새 프로파일 생성" },
|
|
403
|
+
{ label: "프로파일 삭제", hint: "기존 프로파일 제거" },
|
|
404
|
+
{ label: "종료", hint: "Ctrl+C" },
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
async function main() {
|
|
408
|
+
onExit(() => {});
|
|
409
|
+
clear();
|
|
410
|
+
|
|
411
|
+
let config = readConfig();
|
|
412
|
+
|
|
413
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
414
|
+
fail(`config.toml 미존재: ${CONFIG_PATH}`);
|
|
415
|
+
info("codex를 먼저 설치하거나 /tfx-setup을 실행하세요.");
|
|
416
|
+
process.exit(1);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
while (true) {
|
|
420
|
+
box("Codex Profile Manager", 46);
|
|
421
|
+
showStatus(config);
|
|
422
|
+
console.log();
|
|
423
|
+
|
|
424
|
+
const choice = await select("작업 선택", MENU);
|
|
425
|
+
if (!choice || choice.index === 4) {
|
|
426
|
+
console.log();
|
|
427
|
+
info("종료합니다.");
|
|
428
|
+
showCursor();
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
console.log();
|
|
433
|
+
switch (choice.index) {
|
|
434
|
+
case 0:
|
|
435
|
+
config = await editProfile(config);
|
|
436
|
+
break;
|
|
437
|
+
case 1:
|
|
438
|
+
config = await editDefault(config);
|
|
439
|
+
break;
|
|
440
|
+
case 2:
|
|
441
|
+
config = await addProfile(config);
|
|
442
|
+
break;
|
|
443
|
+
case 3:
|
|
444
|
+
config = await removeProfile(config);
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
console.log();
|
|
449
|
+
divider(46);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
main().catch((e) => {
|
|
454
|
+
showCursor();
|
|
455
|
+
console.error(e);
|
|
456
|
+
process.exit(1);
|
|
457
|
+
});
|
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
|
+
}
|