triflux 9.4.0 → 9.5.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "9.3.0",
3
+ "version": "9.5.1",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "author": {
6
6
  "name": "tellang"
package/bin/triflux.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // triflux CLI — setup, doctor, version
3
- import { copyFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync, statSync, openSync, closeSync } from "fs";
3
+ import { copyFileSync, existsSync, readFileSync, readSync, writeFileSync, mkdirSync, chmodSync, readdirSync, unlinkSync, statSync, openSync, closeSync } from "fs";
4
4
  import { join, dirname } from "path";
5
5
  import { homedir } from "os";
6
6
  import { execSync, execFileSync, spawn } from "child_process";
@@ -13,9 +13,9 @@ import { cleanupStaleOmcTeams, inspectStaleOmcTeams } from "../hub/team/staleSta
13
13
  import { getPipelineStateDbPath } from "../hub/pipeline/state.mjs";
14
14
  import { ensureGeminiProfiles } from "../scripts/lib/gemini-profiles.mjs";
15
15
  import {
16
- SYNC_MAP, SKILL_ALIASES, REQUIRED_CODEX_PROFILES,
16
+ SYNC_MAP, SKILL_ALIASES, REQUIRED_CODEX_PROFILES, LEGACY_CODEX_MODELS,
17
17
  syncAliasedSkillDir, hasProfileSection, replaceProfileSection,
18
- ensureCodexProfiles, getVersion,
18
+ ensureCodexProfiles, getVersion, cleanupStaleSkills, DEPRECATED_SKILLS,
19
19
  } from "../scripts/setup.mjs";
20
20
 
21
21
  const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
@@ -720,6 +720,25 @@ function cmdSetup(options = {}) {
720
720
  } else {
721
721
  ok(`스킬: ${skillTotal}개 최신 상태`);
722
722
  }
723
+ // Stale 스킬 정리 (패키지에서 제거된 tfx-* 스킬 삭제)
724
+ const staleCleanup = cleanupStaleSkills(skillsDst, skillsSrc);
725
+ if (staleCleanup.count > 0) {
726
+ ok(`구형 스킬 ${staleCleanup.count}개 제거: ${staleCleanup.removed.join(", ")}`);
727
+ }
728
+ }
729
+
730
+ // ── psmux 기본 셸 자동 수정 (cmd.exe → PowerShell) ──
731
+ if (process.platform === "win32" && which("psmux")) {
732
+ try {
733
+ const shellOut = execSync("psmux show-options -g default-shell 2>NUL", { encoding: "utf8", timeout: 3000 }).trim();
734
+ if (!/powershell|pwsh/i.test(shellOut)) {
735
+ const pwsh = which("pwsh") ? "pwsh" : (which("powershell.exe") ? "powershell.exe" : "");
736
+ if (pwsh) {
737
+ execSync(`psmux set-option -g default-shell "${pwsh}"`, { timeout: 3000, stdio: "pipe" });
738
+ ok(`psmux 기본 셸 → ${pwsh}`);
739
+ }
740
+ }
741
+ } catch { /* psmux 서버 미실행 — 무시 */ }
723
742
  }
724
743
 
725
744
  // ── 결과 추적 ──
@@ -822,22 +841,55 @@ function cmdSetup(options = {}) {
822
841
  }
823
842
  }
824
843
 
825
- // Star request (버전 게이팅)
844
+ // Star request (버전 게이팅 + 인터랙티브 [y/n])
826
845
  const showStar = STAR_PROMPT_VERSIONS.length === 0 || STAR_PROMPT_VERSIONS.includes(PKG.version);
827
846
  if (showStar) {
847
+ let ghOk = false;
828
848
  try {
829
849
  execFileSync("gh", ["auth", "status"], { timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
850
+ ghOk = true;
851
+ } catch {}
852
+
853
+ if (!ghOk) {
854
+ // gh 미설치/미인증 — URL만 표시
855
+ console.log();
856
+ info(`${AMBER}⭐${RESET} 하나가 큰 차이를 만듭니다. ${CYAN}https://github.com/tellang/triflux${RESET}`);
857
+ } else {
858
+ let alreadyStarred = false;
830
859
  try {
831
860
  execFileSync("gh", ["api", "user/starred/tellang/triflux"], { timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
861
+ alreadyStarred = true;
862
+ } catch {}
863
+
864
+ if (alreadyStarred) {
832
865
  console.log();
833
866
  ok(`이미 함께하고 계시군요. ${AMBER}⭐${RESET}`);
834
- } catch {
867
+ } else {
868
+ // 인터랙티브 confirm
835
869
  console.log();
836
- info(`${AMBER}⭐${RESET} 하나가 큰 차이를 만듭니다. ${CYAN}https://github.com/tellang/triflux${RESET}`);
870
+ process.stdout.write(` ${AMBER}⭐${RESET} 하나가 큰 차이를 만듭니다. Star? ${DIM}[y/N]${RESET} `);
871
+ let answer = "";
872
+ try {
873
+ const buf = Buffer.alloc(128);
874
+ const n = readSync(0, buf, 0, 128);
875
+ answer = buf.toString("utf8", 0, n).trim().toLowerCase();
876
+ } catch {
877
+ // non-interactive stdin — 건너뜀
878
+ }
879
+ if (answer.startsWith("y")) {
880
+ try {
881
+ execFileSync("gh", ["api", "-X", "PUT", "/user/starred/tellang/triflux"], {
882
+ timeout: 5000, stdio: ["pipe", "pipe", "pipe"],
883
+ });
884
+ ok(`함께해 주셔서 감사합니다. ${AMBER}⭐${RESET}`);
885
+ } catch {
886
+ info(`${CYAN}https://github.com/tellang/triflux${RESET}`);
887
+ }
888
+ } else if (answer === "") {
889
+ // 아무 입력 없이 Enter — 조용히 URL만
890
+ console.log(` ${DIM}https://github.com/tellang/triflux${RESET}`);
891
+ }
837
892
  }
838
- } catch {
839
- console.log();
840
- info(`${AMBER}⭐${RESET} 하나가 큰 차이를 만듭니다. ${CYAN}https://github.com/tellang/triflux${RESET}`);
841
893
  }
842
894
  }
843
895
 
@@ -1079,7 +1131,9 @@ async function cmdDoctor(options = {}) {
1079
1131
  const missingProfiles = [];
1080
1132
  for (const profile of REQUIRED_CODEX_PROFILES) {
1081
1133
  if (hasProfileSection(codexConfig, profile.name)) {
1082
- ok(`${profile.name}: 정상`);
1134
+ ok(`${profile.name}: 정상${profile.proOnly ? ` ${DIM}(Pro 전용)${RESET}` : ""}`);
1135
+ } else if (profile.proOnly) {
1136
+ info(`${profile.name}: 미설정 ${DIM}(Pro 전용 — Plus/기본에서는 불필요)${RESET}`);
1083
1137
  } else {
1084
1138
  missingProfiles.push(profile.name);
1085
1139
  warn(`${profile.name}: 미설정`);
@@ -1099,6 +1153,18 @@ async function cmdDoctor(options = {}) {
1099
1153
  issues++;
1100
1154
  }
1101
1155
 
1156
+ // Codex 구형 모델 감지
1157
+ if (existsSync(CODEX_CONFIG_PATH)) {
1158
+ const codexContent = readFileSync(CODEX_CONFIG_PATH, "utf8");
1159
+ const legacyFound = LEGACY_CODEX_MODELS.filter(m => codexContent.includes(`"${m}"`));
1160
+ if (legacyFound.length > 0) {
1161
+ warn(`구형 모델 감지: ${legacyFound.join(", ")}`);
1162
+ info("최신 프로필로 마이그레이션: tfx setup 또는 tfx profile");
1163
+ addDoctorCheck(report, { name: "codex-legacy-models", status: "issues", models: legacyFound, fix: "tfx setup" });
1164
+ issues++;
1165
+ }
1166
+ }
1167
+
1102
1168
  // 5. Gemini CLI
1103
1169
  section(`Gemini CLI ${BLUE}●${RESET}`);
1104
1170
  const geminiCli = checkCliCrossShell("gemini", "npm install -g @google/gemini-cli");
@@ -1111,6 +1177,22 @@ async function cmdDoctor(options = {}) {
1111
1177
  });
1112
1178
  // API 키 검사 제거 — bash exec 기반이므로 API 키 불필요
1113
1179
 
1180
+ // Gemini 구형 모델 감지
1181
+ const geminiProfilesPath = join(homedir(), ".gemini", "triflux-profiles.json");
1182
+ const LEGACY_GEMINI_MODELS = ["gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash", "gemini-2.5-pro-preview"];
1183
+ if (existsSync(geminiProfilesPath)) {
1184
+ try {
1185
+ const geminiContent = readFileSync(geminiProfilesPath, "utf8");
1186
+ const geminiLegacy = LEGACY_GEMINI_MODELS.filter(m => geminiContent.includes(m));
1187
+ if (geminiLegacy.length > 0) {
1188
+ warn(`구형 모델 감지: ${geminiLegacy.join(", ")}`);
1189
+ info("최신 프로필로 마이그레이션: tfx setup 또는 tfx profile");
1190
+ addDoctorCheck(report, { name: "gemini-legacy-models", status: "issues", models: geminiLegacy, fix: "tfx setup" });
1191
+ issues++;
1192
+ }
1193
+ } catch {}
1194
+ }
1195
+
1114
1196
  // 6. Claude Code
1115
1197
  section(`Claude Code ${AMBER}●${RESET}`);
1116
1198
  const claudePath = which("claude");
@@ -1123,7 +1205,53 @@ async function cmdDoctor(options = {}) {
1123
1205
  issues++;
1124
1206
  }
1125
1207
 
1126
- // 7. 스킬 설치 상태
1208
+ // 7. psmux (Windows only)
1209
+ if (process.platform === "win32") {
1210
+ section("psmux (터미널 멀티플렉서)");
1211
+ const psmuxPath = which("psmux");
1212
+ if (psmuxPath) {
1213
+ ok("설치됨");
1214
+ // 기본 셸 확인: psmux 세션의 기본 셸이 PowerShell인지 cmd.exe인지
1215
+ let shellOk = false;
1216
+ try {
1217
+ const defaultShell = execSync("psmux show-options -g default-shell 2>NUL", { encoding: "utf8", timeout: 3000 }).trim();
1218
+ shellOk = /powershell|pwsh/i.test(defaultShell);
1219
+ } catch {
1220
+ // show-options 실패 시 pwsh/powershell 존재 여부로 판단
1221
+ shellOk = !!which("pwsh") || !!which("powershell.exe");
1222
+ }
1223
+ if (shellOk) {
1224
+ ok("기본 셸: PowerShell");
1225
+ addDoctorCheck(report, { name: "psmux", status: "ok", path: psmuxPath, shell: "powershell" });
1226
+ } else {
1227
+ if (fix) {
1228
+ // --fix: PowerShell로 자동 변경
1229
+ const pwshBin = which("pwsh") ? "pwsh" : "powershell.exe";
1230
+ try {
1231
+ execSync(`psmux set-option -g default-shell "${pwshBin}"`, { timeout: 3000, stdio: "pipe" });
1232
+ ok(`기본 셸 → ${pwshBin} 으로 변경 완료`);
1233
+ addDoctorCheck(report, { name: "psmux", status: "ok", path: psmuxPath, shell: pwshBin, fixed: true });
1234
+ report.actions.push("psmux default-shell → " + pwshBin);
1235
+ } catch (e) {
1236
+ fail(`기본 셸 변경 실패: ${e.message}`);
1237
+ addDoctorCheck(report, { name: "psmux", status: "issues", path: psmuxPath, shell: "cmd", fix: `psmux set-option -g default-shell "${pwshBin}"` });
1238
+ issues++;
1239
+ }
1240
+ } else {
1241
+ warn("기본 셸이 cmd.exe — headless 명령 실패 가능");
1242
+ info(`수정: tfx doctor --fix 또는 psmux set-option -g default-shell "powershell.exe"`);
1243
+ addDoctorCheck(report, { name: "psmux", status: "issues", path: psmuxPath, shell: "cmd", fix: "tfx doctor --fix" });
1244
+ issues++;
1245
+ }
1246
+ }
1247
+ } else {
1248
+ info(`미설치 ${GRAY}(선택 — 멀티모델 병렬 실행에 필요)${RESET}`);
1249
+ info(`설치: winget install marlocarlo.psmux`);
1250
+ addDoctorCheck(report, { name: "psmux", status: "skipped", detail: "미설치 (선택)", fix: "winget install marlocarlo.psmux" });
1251
+ }
1252
+ }
1253
+
1254
+ // 8. 스킬 설치 상태
1127
1255
  section("Skills");
1128
1256
  const skillsSrc = join(PKG_ROOT, "skills");
1129
1257
  const skillsDst = join(CLAUDE_DIR, "skills");
@@ -1153,7 +1281,32 @@ async function cmdDoctor(options = {}) {
1153
1281
  addDoctorCheck(report, { name: "skills", status: "missing", installed: 0, total: 0, fix: "패키지 skills 디렉토리를 확인하세요." });
1154
1282
  }
1155
1283
 
1156
- // 8. 플러그인 등록
1284
+ // Stale 스킬 체크
1285
+ const staleSkills = [];
1286
+ const userSkillsDir = join(CLAUDE_DIR, "skills");
1287
+ if (existsSync(userSkillsDir)) {
1288
+ const pkgSkillsDir = join(PKG_ROOT, "skills");
1289
+ const pkgSkills = new Set();
1290
+ if (existsSync(pkgSkillsDir)) {
1291
+ for (const n of readdirSync(pkgSkillsDir)) pkgSkills.add(n);
1292
+ }
1293
+ for (const { alias } of SKILL_ALIASES) pkgSkills.add(alias);
1294
+
1295
+ for (const n of readdirSync(userSkillsDir)) {
1296
+ if (!n.startsWith("tfx-")) continue;
1297
+ if (!pkgSkills.has(n)) staleSkills.push(n);
1298
+ }
1299
+ }
1300
+ if (staleSkills.length > 0) {
1301
+ warn(`구형 스킬 ${staleSkills.length}개 감지: ${staleSkills.join(", ")}`);
1302
+ info("제거: tfx setup 또는 tfx update");
1303
+ addDoctorCheck(report, { name: "stale-skills", status: "issues", skills: staleSkills, fix: "tfx setup" });
1304
+ issues++;
1305
+ } else {
1306
+ addDoctorCheck(report, { name: "stale-skills", status: "ok" });
1307
+ }
1308
+
1309
+ // 9. 플러그인 등록
1157
1310
  section("Plugin");
1158
1311
  const pluginsFile = join(CLAUDE_DIR, "plugins", "installed_plugins.json");
1159
1312
  if (existsSync(pluginsFile)) {
@@ -1171,7 +1324,7 @@ async function cmdDoctor(options = {}) {
1171
1324
  info("플러그인 시스템 감지 안 됨 — npm 단독 사용");
1172
1325
  }
1173
1326
 
1174
- // 9. MCP 인벤토리
1327
+ // 10. MCP 인벤토리
1175
1328
  section("MCP Inventory");
1176
1329
  const mcpCache = join(CLAUDE_DIR, "cache", "mcp-inventory.json");
1177
1330
  if (existsSync(mcpCache)) {
@@ -1240,7 +1393,7 @@ async function cmdDoctor(options = {}) {
1240
1393
  issues++;
1241
1394
  }
1242
1395
 
1243
- // 10. CLI 이슈 트래커
1396
+ // 11. CLI 이슈 트래커
1244
1397
  section("CLI Issues");
1245
1398
  const issuesFile = join(CLAUDE_DIR, "cache", "cli-issues.jsonl");
1246
1399
  if (existsSync(issuesFile)) {
@@ -1320,7 +1473,7 @@ async function cmdDoctor(options = {}) {
1320
1473
  ok("이슈 로그 없음 (정상)");
1321
1474
  }
1322
1475
 
1323
- // 11. Team Sessions
1476
+ // 12. Team Sessions
1324
1477
  section("Team Sessions");
1325
1478
  const teamSessionReport = inspectTeamSessions();
1326
1479
  if (!teamSessionReport.mux) {
@@ -1370,7 +1523,7 @@ async function cmdDoctor(options = {}) {
1370
1523
  }
1371
1524
  }
1372
1525
 
1373
- // 12. OMC stale team 상태
1526
+ // 13. OMC stale team 상태
1374
1527
  section("OMC Stale Teams");
1375
1528
  const omcTeamReport = inspectStaleOmcTeams({
1376
1529
  startDir: process.cwd(),
@@ -1460,7 +1613,7 @@ async function cmdDoctor(options = {}) {
1460
1613
  ok("Windows 전용 검사 — 건너뜀");
1461
1614
  }
1462
1615
 
1463
- // 13. Stale Teams (Claude teams/ + tasks/ 자동 감지)
1616
+ // 14. Stale Teams (Claude teams/ + tasks/ 자동 감지)
1464
1617
  section("Stale Teams");
1465
1618
  const teamsDir = join(CLAUDE_DIR, "teams");
1466
1619
  const tasksDir = join(CLAUDE_DIR, "tasks");
@@ -28,6 +28,23 @@ const PSMUX_BIN = (() => {
28
28
  })();
29
29
  const GIT_BASH = process.env.GIT_BASH_PATH || "C:\\Program Files\\Git\\bin\\bash.exe";
30
30
  const IS_WINDOWS = process.platform === "win32";
31
+
32
+ /** Windows psmux 세션의 기본 셸을 PowerShell로 강제한다 (pwsh7 우선, ps5 fallback). */
33
+ const PWSH_BIN = (() => {
34
+ if (!IS_WINDOWS) return "";
35
+ if (process.env.PSMUX_SHELL) return process.env.PSMUX_SHELL;
36
+ // pwsh 7 우선
37
+ try {
38
+ childProcess.execFileSync("pwsh", ["-NoLogo", "-NoProfile", "-Command", "exit 0"], { stdio: "ignore", timeout: 3000, windowsHide: true });
39
+ return "pwsh";
40
+ } catch { /* not found */ }
41
+ // powershell 5 fallback
42
+ try {
43
+ childProcess.execFileSync("powershell.exe", ["-NoLogo", "-NoProfile", "-Command", "exit 0"], { stdio: "ignore", timeout: 3000, windowsHide: true });
44
+ return "powershell.exe";
45
+ } catch { /* not found */ }
46
+ return ""; // 둘 다 없으면 psmux 기본 셸 사용
47
+ })();
31
48
  const PSMUX_TIMEOUT_MS = 10000;
32
49
  const COMPLETION_PREFIX = "__TRIFLUX_DONE__:";
33
50
  const CAPTURE_ROOT = process.env.PSMUX_CAPTURE_ROOT || join(tmpdir(), "psmux-steering");
@@ -145,7 +162,15 @@ function randomToken(prefix) {
145
162
 
146
163
  function ensurePsmuxInstalled() {
147
164
  if (!hasPsmux()) {
148
- throw new Error("psmux가 설치되어 있지 않습니다.");
165
+ throw new Error(
166
+ "psmux가 설치되어 있지 않습니다.\n\n" +
167
+ "psmux는 Codex/Gemini CLI를 병렬 세션으로 실행하는 터미널 멀티플렉서입니다.\n" +
168
+ "설치 방법 (택 1):\n" +
169
+ " winget install marlocarlo.psmux\n" +
170
+ " scoop install psmux\n" +
171
+ " npm install -g psmux\n\n" +
172
+ "설치 후 터미널을 재시작하세요."
173
+ );
149
174
  }
150
175
  }
151
176
 
@@ -409,7 +434,7 @@ export function createPsmuxSession(sessionName, opts = {}) {
409
434
  const limitedPaneCount = layout === "2x2" ? Math.min(paneCount, 4) : paneCount;
410
435
  const sessionTarget = `${sessionName}:0`;
411
436
 
412
- const leadPane = psmuxExec([
437
+ const newSessionArgs = [
413
438
  "new-session",
414
439
  "-d",
415
440
  "-P",
@@ -421,7 +446,15 @@ export function createPsmuxSession(sessionName, opts = {}) {
421
446
  "220",
422
447
  "-y",
423
448
  "55",
424
- ]);
449
+ ];
450
+ // Windows: psmux 기본 셸이 cmd.exe일 수 있으므로 PowerShell 강제
451
+ if (PWSH_BIN) newSessionArgs.push(PWSH_BIN, "-NoLogo", "-NoProfile");
452
+ const leadPane = psmuxExec(newSessionArgs);
453
+
454
+ // split-window로 생성되는 pane도 동일 셸 사용
455
+ if (PWSH_BIN) {
456
+ try { psmuxExec(["set-option", "-t", sessionName, "default-command", `${PWSH_BIN} -NoLogo -NoProfile`]); } catch { /* 미지원 시 무시 */ }
457
+ }
425
458
 
426
459
  if (layout === "2x2" && limitedPaneCount >= 3) {
427
460
  const rightPane = psmuxExec([
@@ -976,7 +1009,10 @@ export async function waitForCompletion(sessionName, paneNameOrTarget, token, ti
976
1009
  */
977
1010
  export function spawnWorker(sessionName, workerName, cmd) {
978
1011
  if (!hasPsmux()) {
979
- throw new Error("psmux가 설치되어 있지 않습니다. psmux를 먼저 설치하세요.");
1012
+ throw new Error(
1013
+ "psmux가 설치되어 있지 않습니다.\n" +
1014
+ "설치: winget install marlocarlo.psmux (또는 scoop install psmux / npm i -g psmux)"
1015
+ );
980
1016
  }
981
1017
 
982
1018
  // remain-on-exit: 종료된 pane이 즉시 사라지는 것 방지
@@ -1017,7 +1053,7 @@ export function spawnWorker(sessionName, workerName, cmd) {
1017
1053
  */
1018
1054
  export function getWorkerStatus(sessionName, workerName) {
1019
1055
  if (!hasPsmux()) {
1020
- throw new Error("psmux 설치되어 있지 않습니다.");
1056
+ throw new Error("psmux 미설치. 설치: winget install marlocarlo.psmux (또는 npm i -g psmux)");
1021
1057
  }
1022
1058
  try {
1023
1059
  const pane = resolvePane(sessionName, workerName);
@@ -1042,7 +1078,7 @@ export function getWorkerStatus(sessionName, workerName) {
1042
1078
  */
1043
1079
  export function killWorker(sessionName, workerName) {
1044
1080
  if (!hasPsmux()) {
1045
- throw new Error("psmux 설치되어 있지 않습니다.");
1081
+ throw new Error("psmux 미설치. 설치: winget install marlocarlo.psmux (또는 npm i -g psmux)");
1046
1082
  }
1047
1083
  try {
1048
1084
  const { paneId, status } = getWorkerStatus(sessionName, workerName);
@@ -1101,7 +1137,7 @@ export function killWorker(sessionName, workerName) {
1101
1137
  */
1102
1138
  export function captureWorkerOutput(sessionName, workerName, lines = 50) {
1103
1139
  if (!hasPsmux()) {
1104
- throw new Error("psmux 설치되어 있지 않습니다.");
1140
+ throw new Error("psmux 미설치. 설치: winget install marlocarlo.psmux (또는 npm i -g psmux)");
1105
1141
  }
1106
1142
  try {
1107
1143
  const { paneId } = getWorkerStatus(sessionName, workerName);
@@ -35,7 +35,7 @@ try {
35
35
  execFileSync("psmux", ["--version"], { encoding: "utf8", timeout: 2000 });
36
36
  } catch {
37
37
  process.stderr.write(
38
- "ERROR: psmux not found or not executable. Install psmux before running tui-viewer.\n",
38
+ "ERROR: psmux 미설치. 설치: winget install marlocarlo.psmux (또는 npm i -g psmux)\n",
39
39
  );
40
40
  process.exit(1);
41
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "9.4.0",
3
+ "version": "9.5.1",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Gemini, and Claude",
5
5
  "type": "module",
6
6
  "bin": {
@@ -155,7 +155,7 @@ async function main() {
155
155
  }
156
156
 
157
157
  if (!hasPsmux()) {
158
- process.stderr.write("ERROR: psmux 설치되어 있지 않습니다. npm install -g psmux\n");
158
+ process.stderr.write("ERROR: psmux 미설치. 설치: winget install marlocarlo.psmux (또는 npm i -g psmux)\n");
159
159
  process.exit(1);
160
160
  }
161
161
 
package/scripts/setup.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  // - hud-qos-status.mjs를 ~/.claude/hud/에 동기화
5
5
  // - skills/를 ~/.claude/skills/에 동기화
6
6
 
7
- import { copyFileSync, mkdirSync, readFileSync, writeFileSync, readdirSync, existsSync, chmodSync, unlinkSync } from "fs";
7
+ import { copyFileSync, mkdirSync, readFileSync, writeFileSync, readdirSync, existsSync, chmodSync, unlinkSync, rmSync } from "fs";
8
8
  import { join, dirname } from "path";
9
9
  import { homedir } from "os";
10
10
  import { spawn, execFileSync } from "child_process";
@@ -52,6 +52,7 @@ const REQUIRED_CODEX_PROFILES = [
52
52
  'model = "gpt-5.3-codex-spark"',
53
53
  'model_reasoning_effort = "low"',
54
54
  ],
55
+ proOnly: true, // Pro 플랜 전용 — Plus/기본에서는 미동작
55
56
  },
56
57
  ];
57
58
 
@@ -62,6 +63,54 @@ const SKILL_ALIASES = [
62
63
  },
63
64
  ];
64
65
 
66
+ /** 패키지에서 제거된 스킬 목록 — setup/update 시 ~/.claude/skills/에서 자동 삭제 */
67
+ const DEPRECATED_SKILLS = [
68
+ "tfx-eval",
69
+ "tfx-learn",
70
+ "tfx-wrapup",
71
+ ];
72
+
73
+ /** 마이그레이션 대상 구형 Codex 모델 — 이 모델을 사용하는 프로필을 감지하여 안내 */
74
+ const LEGACY_CODEX_MODELS = ["o4-mini", "o3", "o3-pro", "o1", "o1-mini", "o1-pro", "gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano", "codex-mini-latest"];
75
+
76
+ /**
77
+ * ~/.claude/skills/ 에서 패키지에 없는 stale tfx-* 스킬을 제거한다.
78
+ * @param {string} skillsDst - ~/.claude/skills/ 경로
79
+ * @param {string} skillsSrc - 패키지의 skills/ 경로
80
+ * @returns {{ removed: string[], count: number }}
81
+ */
82
+ function cleanupStaleSkills(skillsDst, skillsSrc) {
83
+ const removed = [];
84
+ if (!existsSync(skillsDst)) return { removed, count: 0 };
85
+
86
+ const packageSkills = new Set();
87
+ if (existsSync(skillsSrc)) {
88
+ for (const name of readdirSync(skillsSrc)) {
89
+ packageSkills.add(name);
90
+ }
91
+ }
92
+ // aliases도 유효한 스킬로 등록
93
+ for (const { alias } of SKILL_ALIASES) {
94
+ packageSkills.add(alias);
95
+ }
96
+
97
+ for (const name of readdirSync(skillsDst)) {
98
+ // tfx- 접두사가 아닌 스킬은 사용자 커스텀 — 건드리지 않음
99
+ if (!name.startsWith("tfx-")) continue;
100
+ if (packageSkills.has(name)) continue;
101
+
102
+ // 패키지에 없는 tfx-* 스킬 → 삭제
103
+ const skillDir = join(skillsDst, name);
104
+ try {
105
+ rmSync(skillDir, { recursive: true, force: true });
106
+ removed.push(name);
107
+ } catch {
108
+ // 삭제 실패 시 무시
109
+ }
110
+ }
111
+ return { removed, count: removed.length };
112
+ }
113
+
65
114
  function buildAliasedSkillContent(srcContent, { alias, source }) {
66
115
  return srcContent
67
116
  .replace(/^name:\s*.+$/m, `name: ${alias}`)
@@ -366,7 +415,9 @@ export {
366
415
  replaceProfileSection, hasProfileSection, escapeRegExp, detectDevMode,
367
416
  SYNC_MAP, BREADCRUMB_PATH, PLUGIN_ROOT, CLAUDE_DIR,
368
417
  SKILL_ALIASES, REQUIRED_CODEX_PROFILES,
418
+ DEPRECATED_SKILLS, LEGACY_CODEX_MODELS,
369
419
  buildAliasedSkillContent, syncAliasedSkillDir, getVersion, ensureCodexProfiles,
420
+ cleanupStaleSkills,
370
421
  };
371
422
 
372
423
  async function main() {
@@ -769,24 +820,70 @@ if (existsSync(HUB_PID_FILE)) {
769
820
  }
770
821
 
771
822
  // ── psmux 자동 설치 (Windows, headless 모드용) ──
823
+ // psmux: Windows용 터미널 멀티플렉서. Codex/Gemini CLI를 병렬 세션으로 실행할 때 필요.
824
+ // 없어도 triflux 기본 기능은 동작하지만, headless 멀티모델 오케스트레이션이 비활성화됨.
772
825
 
826
+ let psmuxInstalled = false;
773
827
  if (process.platform === "win32") {
774
828
  try {
775
829
  execFileSync("where", ["psmux"], { stdio: "ignore" });
830
+ psmuxInstalled = true;
776
831
  } catch {
777
832
  // psmux 미설치 — winget으로 자동 설치 시도
778
- console.log(" psmux 미설치 — winget으로 설치 중...");
833
+ console.log(" psmux 미설치 — 자동 설치 시도 중...");
779
834
  try {
780
835
  execFileSync("winget", ["install", "--id", "marlocarlo.psmux", "--accept-package-agreements", "--accept-source-agreements"], {
781
836
  stdio: ["ignore", "pipe", "pipe"],
782
837
  timeout: 60000,
783
838
  });
784
839
  console.log(" \x1b[32m✓\x1b[0m psmux 설치 완료");
840
+ psmuxInstalled = true;
785
841
  synced++;
786
842
  } catch {
787
- console.log(" \x1b[33m⚠\x1b[0m psmux 자동 설치 실패 — 수동 설치: winget install psmux");
843
+ console.log([
844
+ " \x1b[33m⚠\x1b[0m psmux 자동 설치 실패 — 수동 설치 방법:",
845
+ " \x1b[36m옵션 1:\x1b[0m winget install marlocarlo.psmux",
846
+ " \x1b[36m옵션 2:\x1b[0m scoop install psmux",
847
+ " \x1b[36m옵션 3:\x1b[0m npm install -g psmux",
848
+ " \x1b[2m(없어도 기본 기능은 동작합니다 — 멀티모델 병렬 실행만 비활성화)\x1b[0m",
849
+ ].join("\n"));
788
850
  }
789
851
  }
852
+ } else {
853
+ // non-Windows: tmux 사용 (psmux 불필요)
854
+ psmuxInstalled = true;
855
+ }
856
+
857
+ // ── psmux 기본 셸 자동 수정 (cmd.exe → PowerShell) ──
858
+ if (psmuxInstalled && process.platform === "win32") {
859
+ try {
860
+ const shellOut = execFileSync("psmux", ["show-options", "-g", "default-shell"], { encoding: "utf8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"] }).trim();
861
+ if (!/powershell|pwsh/i.test(shellOut)) {
862
+ // pwsh(7) 우선, powershell.exe(5) fallback
863
+ let pwsh = "";
864
+ try { execFileSync("where", ["pwsh"], { stdio: "ignore" }); pwsh = "pwsh"; } catch {
865
+ try { execFileSync("where", ["powershell.exe"], { stdio: "ignore" }); pwsh = "powershell.exe"; } catch {}
866
+ }
867
+ if (pwsh) {
868
+ execFileSync("psmux", ["set-option", "-g", "default-shell", pwsh], { timeout: 3000, stdio: "ignore" });
869
+ console.log(` \x1b[32m✓\x1b[0m psmux 기본 셸 → ${pwsh}`);
870
+ synced++;
871
+ }
872
+ }
873
+ } catch {
874
+ // psmux show-options 미지원 또는 서버 미실행 — 무시
875
+ }
876
+ }
877
+
878
+ // ── stale 스킬 정리 (패키지에서 제거된 tfx-* 스킬 삭제) ──
879
+ {
880
+ const skillsDst = join(CLAUDE_DIR, "skills");
881
+ const skillsSrc = join(PLUGIN_ROOT, "skills");
882
+ const cleanup = cleanupStaleSkills(skillsDst, skillsSrc);
883
+ if (cleanup.count > 0) {
884
+ console.log(` \x1b[32m✓\x1b[0m ${cleanup.count}개 구형 스킬 제거: ${cleanup.removed.join(", ")}`);
885
+ synced++;
886
+ }
790
887
  }
791
888
 
792
889
  // ── HUD 에러 캐시 자동 클리어 (업데이트/재설치 시) ──
@@ -907,6 +1004,7 @@ ${B}╚════════════════════════
907
1004
  ${G}✓${R} hud-qos-status → ~/.claude/hud/
908
1005
  ${G}✓${R} ${synced > 0 ? synced + " files synced" : "all files up to date"}
909
1006
  ${G}✓${R} HUD statusLine → settings.json
1007
+ ${psmuxInstalled ? `${G}✓${R} psmux → headless 멀티모델 오케스트레이션` : `${Y}○${R} psmux 미설치 → ${D}winget install marlocarlo.psmux${R} ${D}(선택)${R}`}
910
1008
 
911
1009
  ${B}Commands:${R}
912
1010
  ${C}triflux${R} setup 파일 동기화 + HUD 설정
@@ -930,6 +1028,19 @@ ${B}Skills (Claude Code):${R}
930
1028
  ${Y}!${R} 세션 재시작 후 스킬이 활성화됩니다
931
1029
  ${D}https://github.com/tellang/triflux${R}
932
1030
  `);
1031
+
1032
+ // ── GitHub Star 체크 (비인터랙티브 — postinstall에서는 confirm 불가) ──
1033
+ try {
1034
+ execFileSync("gh", ["auth", "status"], { timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1035
+ try {
1036
+ execFileSync("gh", ["api", "user/starred/tellang/triflux"], { timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
1037
+ console.log(` ${G}⭐${R} 이미 함께하고 계시군요.`);
1038
+ } catch {
1039
+ console.log(` ${Y}⭐${R} 하나가 큰 차이를 만듭니다. ${D}https://github.com/tellang/triflux${R}`);
1040
+ }
1041
+ } catch {
1042
+ // gh 미설치/미인증 — 무시
1043
+ }
933
1044
  }
934
1045
 
935
1046
  process.exit(0);