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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/triflux.mjs +170 -17
- package/hub/team/psmux.mjs +43 -7
- package/hub/team/tui-viewer.mjs +1 -1
- package/package.json +1 -1
- package/scripts/session-spawn-helper.mjs +1 -1
- package/scripts/setup.mjs +114 -3
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
|
-
}
|
|
867
|
+
} else {
|
|
868
|
+
// 인터랙티브 confirm
|
|
835
869
|
console.log();
|
|
836
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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");
|
package/hub/team/psmux.mjs
CHANGED
|
@@ -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(
|
|
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
|
|
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(
|
|
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);
|
package/hub/team/tui-viewer.mjs
CHANGED
|
@@ -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
|
|
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
|
@@ -155,7 +155,7 @@ async function main() {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
if (!hasPsmux()) {
|
|
158
|
-
process.stderr.write("ERROR: psmux
|
|
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 미설치 —
|
|
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(
|
|
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);
|