triflux 9.3.0 → 9.5.0
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 +171 -231
- package/hub/pipe.mjs +1 -8
- package/hub/team/psmux.mjs +64 -8
- package/hub/team/tui-viewer.mjs +1 -1
- package/hub/workers/claude-worker.mjs +1 -24
- package/hub/workers/codex-mcp.mjs +0 -4
- package/hub/workers/gemini-worker.mjs +108 -28
- package/hub/workers/interface.mjs +0 -1
- package/hub/workers/worker-utils.mjs +26 -0
- package/package.json +1 -1
- package/scripts/__tests__/remote-spawn.test.mjs +17 -3
- package/scripts/cross-review-gate.mjs +11 -65
- package/scripts/cross-review-tracker.mjs +10 -51
- package/scripts/headless-guard.mjs +5 -17
- package/scripts/lib/cross-review-utils.mjs +51 -0
- package/scripts/lib/hook-utils.mjs +14 -0
- package/scripts/lib/mcp-filter.mjs +10 -1
- package/scripts/psmux-safety-guard.mjs +64 -0
- package/scripts/remote-spawn.mjs +77 -23
- package/scripts/session-spawn-helper.mjs +3 -2
- package/scripts/setup.mjs +129 -9
- package/scripts/tfx-route.sh +93 -10
- package/skills/tfx-psmux-rules/SKILL.md +100 -13
- package/skills/tfx-research/SKILL.md +1 -1
- package/skills/tfx-setup/SKILL.md +17 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
|
|
3
|
+
export const SESSION_TTL_SEC = 30 * 60;
|
|
4
|
+
export const STATE_REL_PATH = join(".omc", "state", "cross-review.json");
|
|
5
|
+
|
|
6
|
+
export function readStdin() {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
let raw = "";
|
|
9
|
+
process.stdin.setEncoding("utf8");
|
|
10
|
+
process.stdin.on("data", (chunk) => {
|
|
11
|
+
raw += chunk;
|
|
12
|
+
});
|
|
13
|
+
process.stdin.on("end", () => resolve(raw));
|
|
14
|
+
process.stdin.on("error", () => resolve(""));
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseJson(raw) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function nowSec() {
|
|
27
|
+
return Math.floor(Date.now() / 1000);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function resolveBaseDir(payload) {
|
|
31
|
+
if (typeof payload?.cwd === "string" && payload.cwd.trim()) return payload.cwd;
|
|
32
|
+
if (typeof payload?.directory === "string" && payload.directory.trim()) return payload.directory;
|
|
33
|
+
return process.cwd();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function shouldTrackPath(filePath) {
|
|
37
|
+
if (typeof filePath !== "string" || !filePath.trim()) return false;
|
|
38
|
+
|
|
39
|
+
const lower = filePath.toLowerCase();
|
|
40
|
+
if (lower.startsWith(".omc/") || lower.startsWith(".claude/")) return false;
|
|
41
|
+
if (lower === "package-lock.json" || lower.endsWith("/package-lock.json")) return false;
|
|
42
|
+
if (/\.(md|lock|yml|yaml)$/i.test(lower)) return false;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function expectedReviewer(author) {
|
|
47
|
+
if (author === "claude") return "codex";
|
|
48
|
+
if (author === "codex") return "claude";
|
|
49
|
+
if (author === "gemini") return "claude";
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function nudge(message) {
|
|
2
|
+
process.stdout.write(JSON.stringify({
|
|
3
|
+
hookSpecificOutput: {
|
|
4
|
+
hookEventName: "PreToolUse",
|
|
5
|
+
additionalContext: message,
|
|
6
|
+
},
|
|
7
|
+
}));
|
|
8
|
+
process.exit(0);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function deny(reason) {
|
|
12
|
+
process.stderr.write(reason);
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
@@ -369,6 +369,13 @@ function selectContextualServers(baseServers, profile, options = {}) {
|
|
|
369
369
|
const selected = new Set(
|
|
370
370
|
(profile.alwaysOnServers || []).filter((server) => baseServers.includes(server)),
|
|
371
371
|
);
|
|
372
|
+
const wantsBrowserObservation = (
|
|
373
|
+
baseServers.includes('playwright')
|
|
374
|
+
&& /(?:browser|screenshot|layout|responsive|visual|screen|page|ui|ux|regression|캡처|스크린샷|레이아웃|반응형|화면|브라우저)/iu.test(taskText)
|
|
375
|
+
);
|
|
376
|
+
if (wantsBrowserObservation) {
|
|
377
|
+
selected.add('playwright');
|
|
378
|
+
}
|
|
372
379
|
const requestedSearchTool = typeof options.searchTool === 'string' ? options.searchTool : '';
|
|
373
380
|
|
|
374
381
|
const rankedServers = rankServers(
|
|
@@ -477,7 +484,9 @@ export function resolveAllowedServers(options = {}) {
|
|
|
477
484
|
const baseServers = availableServers.length
|
|
478
485
|
? profile.allowedServers.filter((server) => availableServers.includes(server))
|
|
479
486
|
: [...profile.allowedServers];
|
|
480
|
-
const manifestFiltered =
|
|
487
|
+
const manifestFiltered = availableServers.length
|
|
488
|
+
? baseServers
|
|
489
|
+
: applyManifestFilter(baseServers);
|
|
481
490
|
return selectContextualServers(manifestFiltered, profile, { ...options, inventory, inventoryIndex });
|
|
482
491
|
}
|
|
483
492
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* psmux-safety-guard.mjs — PreToolUse 훅
|
|
4
|
+
*
|
|
5
|
+
* psmux kill-session 직접 호출을 차단하고,
|
|
6
|
+
* psmux/wt 명령 사용 시 안전 규칙을 강제 주입한다.
|
|
7
|
+
*
|
|
8
|
+
* 동작:
|
|
9
|
+
* - kill-session 직접 호출 → deny (exit 2)
|
|
10
|
+
* - psmux/wt 명령 감지 → additionalContext에 안전 규칙 주입
|
|
11
|
+
* - 그 외 → 통과 (exit 0)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { nudge, deny } from "./lib/hook-utils.mjs";
|
|
15
|
+
|
|
16
|
+
const KILL_PATTERN = /\bpsmux\s+kill-session\b/;
|
|
17
|
+
const GRACEFUL_PATTERN = /send-keys.*exit.*Enter[\s\S]*sleep\s+\d/;
|
|
18
|
+
const PSMUX_OR_WT = /\b(psmux|wt\.exe|wt\s)/;
|
|
19
|
+
|
|
20
|
+
const SAFETY_RULES = `[psmux-safety] WT 프리징 방지 필수 규칙:
|
|
21
|
+
1) exit → sleep 2 → kill 순서 필수. 바로 kill 절대 금지.
|
|
22
|
+
2) psmux send-keys는 PowerShell 구문만 (bash 문법 직접 전달 금지).
|
|
23
|
+
3) 경로는 Windows 형식 (C:\\...). /c/... 금지.
|
|
24
|
+
4) wt.exe는 sp(split-pane)만 사용. nt(new-tab) 금지.
|
|
25
|
+
5) -p triflux 프로파일 필수.`;
|
|
26
|
+
|
|
27
|
+
async function main() {
|
|
28
|
+
let raw = "";
|
|
29
|
+
for await (const chunk of process.stdin) raw += chunk;
|
|
30
|
+
if (!raw.trim()) process.exit(0);
|
|
31
|
+
|
|
32
|
+
let input;
|
|
33
|
+
try { input = JSON.parse(raw); } catch { process.exit(0); }
|
|
34
|
+
|
|
35
|
+
if (input.tool_name !== "Bash") process.exit(0);
|
|
36
|
+
|
|
37
|
+
const cmd = input.tool_input?.command || "";
|
|
38
|
+
|
|
39
|
+
// kill-session 직접 호출 감지
|
|
40
|
+
if (KILL_PATTERN.test(cmd)) {
|
|
41
|
+
// 같은 명령 블록 안에 graceful exit 패턴이 있으면 허용
|
|
42
|
+
if (GRACEFUL_PATTERN.test(cmd)) {
|
|
43
|
+
nudge(SAFETY_RULES);
|
|
44
|
+
}
|
|
45
|
+
deny(
|
|
46
|
+
"[psmux-safety] psmux kill-session 직접 호출 차단.\n" +
|
|
47
|
+
"WT 프리징을 방지하려면 반드시 exit → sleep 2 → kill 순서를 따르세요.\n\n" +
|
|
48
|
+
"올바른 패턴:\n" +
|
|
49
|
+
" psmux send-keys -t SESSION \"exit\" Enter\n" +
|
|
50
|
+
" sleep 2\n" +
|
|
51
|
+
" psmux kill-session -t SESSION\n\n" +
|
|
52
|
+
"이 3줄을 하나의 명령 블록으로 실행하세요."
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// psmux/wt 명령 감지 → 안전 규칙 주입
|
|
57
|
+
if (PSMUX_OR_WT.test(cmd)) {
|
|
58
|
+
nudge(SAFETY_RULES);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
main().catch(() => process.exit(0));
|
package/scripts/remote-spawn.mjs
CHANGED
|
@@ -915,48 +915,100 @@ function isPrimaryPaneDead(paneId) {
|
|
|
915
915
|
}
|
|
916
916
|
}
|
|
917
917
|
|
|
918
|
-
async function
|
|
919
|
-
const
|
|
920
|
-
const
|
|
918
|
+
async function watchSpawnSessionExit(sessionName, options = {}) {
|
|
919
|
+
const paneId = options.paneId || `${sessionName}:0.0`;
|
|
920
|
+
const pollMs = parsePositiveInt(options.pollMs, DEFAULT_CLEANUP_WATCH_POLL_MS);
|
|
921
|
+
const graceMs = parsePositiveInt(options.graceMs, DEFAULT_CLEANUP_WATCH_GRACE_MS);
|
|
922
|
+
const maxWaitMs = parsePositiveInt(options.maxWaitMs, DEFAULT_CLEANUP_WATCH_MAX_MS);
|
|
923
|
+
const sessionExists = typeof options.sessionExists === "function"
|
|
924
|
+
? options.sessionExists
|
|
925
|
+
: psmuxSessionExists;
|
|
926
|
+
const getPaneStatus = typeof options.getPaneStatus === "function"
|
|
927
|
+
? options.getPaneStatus
|
|
928
|
+
: (targetPaneId) => ({ isDead: isPrimaryPaneDead(targetPaneId), exitCode: null });
|
|
929
|
+
const killSession = typeof options.killSession === "function"
|
|
930
|
+
? options.killSession
|
|
931
|
+
: killPsmuxSession;
|
|
932
|
+
const now = typeof options.now === "function" ? options.now : Date.now;
|
|
933
|
+
const sleep = typeof options.sleep === "function" ? options.sleep : sleepMsAsync;
|
|
934
|
+
const startedAt = now();
|
|
921
935
|
let consecutiveErrors = 0;
|
|
922
936
|
|
|
923
|
-
while (
|
|
924
|
-
if (!
|
|
925
|
-
return;
|
|
937
|
+
while (now() - startedAt <= maxWaitMs) {
|
|
938
|
+
if (!sessionExists(sessionName)) {
|
|
939
|
+
return { cleaned: false, reason: "session-missing" };
|
|
926
940
|
}
|
|
927
941
|
|
|
928
|
-
const
|
|
929
|
-
if (
|
|
942
|
+
const paneStatus = getPaneStatus(paneId) || {};
|
|
943
|
+
if (paneStatus.isDead == null) {
|
|
930
944
|
consecutiveErrors += 1;
|
|
931
|
-
if (consecutiveErrors >= 10)
|
|
945
|
+
if (consecutiveErrors >= 10) {
|
|
946
|
+
return { cleaned: false, reason: "probe-failed" };
|
|
947
|
+
}
|
|
932
948
|
} else {
|
|
933
949
|
consecutiveErrors = 0;
|
|
934
950
|
}
|
|
935
|
-
if (dead === true) {
|
|
936
|
-
await sleepMsAsync(timings.graceMs);
|
|
937
951
|
|
|
938
|
-
|
|
939
|
-
|
|
952
|
+
if (paneStatus.isDead === true) {
|
|
953
|
+
await sleep(graceMs);
|
|
954
|
+
|
|
955
|
+
if (!sessionExists(sessionName)) {
|
|
956
|
+
return { cleaned: false, reason: "session-missing" };
|
|
940
957
|
}
|
|
941
958
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
959
|
+
const afterGrace = getPaneStatus(paneId) || {};
|
|
960
|
+
if (afterGrace.isDead === true) {
|
|
961
|
+
killSession(sessionName);
|
|
962
|
+
return {
|
|
963
|
+
cleaned: true,
|
|
964
|
+
reason: "pane-dead",
|
|
965
|
+
exitCode: afterGrace.exitCode ?? paneStatus.exitCode ?? null,
|
|
966
|
+
};
|
|
945
967
|
}
|
|
946
968
|
}
|
|
947
969
|
|
|
948
|
-
await
|
|
970
|
+
await sleep(pollMs);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
return { cleaned: false, reason: "timeout" };
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
async function runSpawnCleanupWatcher(sessionName, paneId, timingOptions = {}) {
|
|
977
|
+
await watchSpawnSessionExit(sessionName, {
|
|
978
|
+
paneId,
|
|
979
|
+
pollMs: timingOptions.pollMs,
|
|
980
|
+
graceMs: timingOptions.graceMs,
|
|
981
|
+
maxWaitMs: timingOptions.maxMs,
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function startSpawnExitWatcher(sessionName, options = {}) {
|
|
986
|
+
if (!options.force && !shouldUsePsmux()) {
|
|
987
|
+
return false;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const paneId = options.paneId || `${sessionName}:0.0`;
|
|
991
|
+
const args = buildSpawnCleanupWatcherArgs(sessionName, paneId, {
|
|
992
|
+
graceMs: options.graceMs,
|
|
993
|
+
maxMs: options.maxMs,
|
|
994
|
+
pollMs: options.pollMs,
|
|
995
|
+
});
|
|
996
|
+
args[0] = options.scriptPath || args[0];
|
|
997
|
+
const spawnFn = typeof options.spawnFn === "function" ? options.spawnFn : spawn;
|
|
998
|
+
const child = spawnFn(options.execPath || process.execPath, args, {
|
|
999
|
+
detached: true,
|
|
1000
|
+
stdio: "ignore",
|
|
1001
|
+
windowsHide: true,
|
|
1002
|
+
});
|
|
1003
|
+
if (child && typeof child.unref === "function") {
|
|
1004
|
+
child.unref();
|
|
949
1005
|
}
|
|
1006
|
+
return true;
|
|
950
1007
|
}
|
|
951
1008
|
|
|
952
1009
|
function startSpawnSessionCleanupWatcher(sessionName, paneId, timingOptions = {}) {
|
|
953
|
-
const args = buildSpawnCleanupWatcherArgs(sessionName, paneId, timingOptions);
|
|
954
1010
|
try {
|
|
955
|
-
|
|
956
|
-
detached: true,
|
|
957
|
-
stdio: "ignore",
|
|
958
|
-
windowsHide: true,
|
|
959
|
-
}).unref();
|
|
1011
|
+
startSpawnExitWatcher(sessionName, { ...timingOptions, force: true, paneId });
|
|
960
1012
|
} catch {
|
|
961
1013
|
// watcher 시작 실패는 spawn 자체 실패로 보지 않는다.
|
|
962
1014
|
}
|
|
@@ -1230,6 +1282,8 @@ export const __remoteSpawnTest = {
|
|
|
1230
1282
|
buildPromptContext,
|
|
1231
1283
|
parseArgs,
|
|
1232
1284
|
rewritePromptPaths,
|
|
1285
|
+
startSpawnExitWatcher,
|
|
1233
1286
|
stageRemotePromptFiles,
|
|
1234
1287
|
validateTransferCandidate,
|
|
1288
|
+
watchSpawnSessionExit,
|
|
1235
1289
|
};
|
|
@@ -69,10 +69,11 @@ export function createIsolatedSession(options = {}) {
|
|
|
69
69
|
export function attachWithWindowsTerminal(sessionName, options = {}) {
|
|
70
70
|
const profile = options.profile || DEFAULT_ATTACH_PROFILE;
|
|
71
71
|
const title = options.title || sessionName;
|
|
72
|
+
const spawnFn = options.spawnFn || spawn;
|
|
72
73
|
|
|
73
74
|
// sp (split-pane), not new-tab
|
|
74
75
|
const wtArgs = ["sp", "-p", profile, "--title", title, "--", "psmux", "attach-session", "-t", sessionName];
|
|
75
|
-
const child =
|
|
76
|
+
const child = spawnFn("wt.exe", wtArgs, { detached: true, stdio: "ignore", windowsHide: false });
|
|
76
77
|
child.unref();
|
|
77
78
|
return wtArgs;
|
|
78
79
|
}
|
|
@@ -154,7 +155,7 @@ async function main() {
|
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
if (!hasPsmux()) {
|
|
157
|
-
process.stderr.write("ERROR: psmux
|
|
158
|
+
process.stderr.write("ERROR: psmux 미설치. 설치: winget install marlocarlo.psmux (또는 npm i -g psmux)\n");
|
|
158
159
|
process.exit(1);
|
|
159
160
|
}
|
|
160
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}`)
|
|
@@ -141,6 +190,11 @@ const SYNC_MAP = [
|
|
|
141
190
|
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "claude-worker.mjs"),
|
|
142
191
|
label: "hub/workers/claude-worker.mjs",
|
|
143
192
|
},
|
|
193
|
+
{
|
|
194
|
+
src: join(PLUGIN_ROOT, "hub", "workers", "worker-utils.mjs"),
|
|
195
|
+
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "worker-utils.mjs"),
|
|
196
|
+
label: "hub/workers/worker-utils.mjs",
|
|
197
|
+
},
|
|
144
198
|
{
|
|
145
199
|
src: join(PLUGIN_ROOT, "hub", "workers", "factory.mjs"),
|
|
146
200
|
dst: join(CLAUDE_DIR, "scripts", "hub", "workers", "factory.mjs"),
|
|
@@ -216,6 +270,26 @@ const SYNC_MAP = [
|
|
|
216
270
|
dst: join(CLAUDE_DIR, "scripts", "lib", "mcp-server-catalog.mjs"),
|
|
217
271
|
label: "lib/mcp-server-catalog.mjs",
|
|
218
272
|
},
|
|
273
|
+
{
|
|
274
|
+
src: join(PLUGIN_ROOT, "scripts", "lib", "mcp-manifest.mjs"),
|
|
275
|
+
dst: join(CLAUDE_DIR, "scripts", "lib", "mcp-manifest.mjs"),
|
|
276
|
+
label: "lib/mcp-manifest.mjs",
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
src: join(PLUGIN_ROOT, "scripts", "lib", "hook-utils.mjs"),
|
|
280
|
+
dst: join(CLAUDE_DIR, "scripts", "lib", "hook-utils.mjs"),
|
|
281
|
+
label: "lib/hook-utils.mjs",
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
src: join(PLUGIN_ROOT, "scripts", "psmux-safety-guard.mjs"),
|
|
285
|
+
dst: join(CLAUDE_DIR, "scripts", "psmux-safety-guard.mjs"),
|
|
286
|
+
label: "psmux-safety-guard.mjs",
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
src: join(PLUGIN_ROOT, "scripts", "lib", "cross-review-utils.mjs"),
|
|
290
|
+
dst: join(CLAUDE_DIR, "scripts", "lib", "cross-review-utils.mjs"),
|
|
291
|
+
label: "lib/cross-review-utils.mjs",
|
|
292
|
+
},
|
|
219
293
|
{
|
|
220
294
|
src: join(PLUGIN_ROOT, "scripts", "lib", "keyword-rules.mjs"),
|
|
221
295
|
dst: join(CLAUDE_DIR, "scripts", "lib", "keyword-rules.mjs"),
|
|
@@ -331,13 +405,20 @@ function ensureCodexProfiles() {
|
|
|
331
405
|
writeFileSync(CODEX_CONFIG_PATH, updated, "utf8");
|
|
332
406
|
}
|
|
333
407
|
|
|
334
|
-
return changed;
|
|
335
|
-
} catch {
|
|
336
|
-
return 0;
|
|
408
|
+
return { ok: true, changed };
|
|
409
|
+
} catch (error) {
|
|
410
|
+
return { ok: false, changed: 0, message: error.message };
|
|
337
411
|
}
|
|
338
412
|
}
|
|
339
413
|
|
|
340
|
-
export {
|
|
414
|
+
export {
|
|
415
|
+
replaceProfileSection, hasProfileSection, escapeRegExp, detectDevMode,
|
|
416
|
+
SYNC_MAP, BREADCRUMB_PATH, PLUGIN_ROOT, CLAUDE_DIR,
|
|
417
|
+
SKILL_ALIASES, REQUIRED_CODEX_PROFILES,
|
|
418
|
+
DEPRECATED_SKILLS, LEGACY_CODEX_MODELS,
|
|
419
|
+
buildAliasedSkillContent, syncAliasedSkillDir, getVersion, ensureCodexProfiles,
|
|
420
|
+
cleanupStaleSkills,
|
|
421
|
+
};
|
|
341
422
|
|
|
342
423
|
async function main() {
|
|
343
424
|
const isSync = process.argv.includes("--sync");
|
|
@@ -739,24 +820,49 @@ if (existsSync(HUB_PID_FILE)) {
|
|
|
739
820
|
}
|
|
740
821
|
|
|
741
822
|
// ── psmux 자동 설치 (Windows, headless 모드용) ──
|
|
823
|
+
// psmux: Windows용 터미널 멀티플렉서. Codex/Gemini CLI를 병렬 세션으로 실행할 때 필요.
|
|
824
|
+
// 없어도 triflux 기본 기능은 동작하지만, headless 멀티모델 오케스트레이션이 비활성화됨.
|
|
742
825
|
|
|
826
|
+
let psmuxInstalled = false;
|
|
743
827
|
if (process.platform === "win32") {
|
|
744
828
|
try {
|
|
745
829
|
execFileSync("where", ["psmux"], { stdio: "ignore" });
|
|
830
|
+
psmuxInstalled = true;
|
|
746
831
|
} catch {
|
|
747
832
|
// psmux 미설치 — winget으로 자동 설치 시도
|
|
748
|
-
console.log(" psmux 미설치 —
|
|
833
|
+
console.log(" psmux 미설치 — 자동 설치 시도 중...");
|
|
749
834
|
try {
|
|
750
835
|
execFileSync("winget", ["install", "--id", "marlocarlo.psmux", "--accept-package-agreements", "--accept-source-agreements"], {
|
|
751
836
|
stdio: ["ignore", "pipe", "pipe"],
|
|
752
837
|
timeout: 60000,
|
|
753
838
|
});
|
|
754
839
|
console.log(" \x1b[32m✓\x1b[0m psmux 설치 완료");
|
|
840
|
+
psmuxInstalled = true;
|
|
755
841
|
synced++;
|
|
756
842
|
} catch {
|
|
757
|
-
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"));
|
|
758
850
|
}
|
|
759
851
|
}
|
|
852
|
+
} else {
|
|
853
|
+
// non-Windows: tmux 사용 (psmux 불필요)
|
|
854
|
+
psmuxInstalled = true;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// ── stale 스킬 정리 (패키지에서 제거된 tfx-* 스킬 삭제) ──
|
|
858
|
+
{
|
|
859
|
+
const skillsDst = join(CLAUDE_DIR, "skills");
|
|
860
|
+
const skillsSrc = join(PLUGIN_ROOT, "skills");
|
|
861
|
+
const cleanup = cleanupStaleSkills(skillsDst, skillsSrc);
|
|
862
|
+
if (cleanup.count > 0) {
|
|
863
|
+
console.log(` \x1b[32m✓\x1b[0m ${cleanup.count}개 구형 스킬 제거: ${cleanup.removed.join(", ")}`);
|
|
864
|
+
synced++;
|
|
865
|
+
}
|
|
760
866
|
}
|
|
761
867
|
|
|
762
868
|
// ── HUD 에러 캐시 자동 클리어 (업데이트/재설치 시) ──
|
|
@@ -814,8 +920,8 @@ if (process.platform === "win32") {
|
|
|
814
920
|
|
|
815
921
|
// ── Codex 프로필 자동 보정 ──
|
|
816
922
|
|
|
817
|
-
const
|
|
818
|
-
if (
|
|
923
|
+
const codexProfileResult = ensureCodexProfiles();
|
|
924
|
+
if (codexProfileResult.changed > 0) {
|
|
819
925
|
synced++;
|
|
820
926
|
}
|
|
821
927
|
|
|
@@ -877,6 +983,7 @@ ${B}╚════════════════════════
|
|
|
877
983
|
${G}✓${R} hud-qos-status → ~/.claude/hud/
|
|
878
984
|
${G}✓${R} ${synced > 0 ? synced + " files synced" : "all files up to date"}
|
|
879
985
|
${G}✓${R} HUD statusLine → settings.json
|
|
986
|
+
${psmuxInstalled ? `${G}✓${R} psmux → headless 멀티모델 오케스트레이션` : `${Y}○${R} psmux 미설치 → ${D}winget install marlocarlo.psmux${R} ${D}(선택)${R}`}
|
|
880
987
|
|
|
881
988
|
${B}Commands:${R}
|
|
882
989
|
${C}triflux${R} setup 파일 동기화 + HUD 설정
|
|
@@ -900,6 +1007,19 @@ ${B}Skills (Claude Code):${R}
|
|
|
900
1007
|
${Y}!${R} 세션 재시작 후 스킬이 활성화됩니다
|
|
901
1008
|
${D}https://github.com/tellang/triflux${R}
|
|
902
1009
|
`);
|
|
1010
|
+
|
|
1011
|
+
// ── GitHub Star 체크 (비인터랙티브 — postinstall에서는 confirm 불가) ──
|
|
1012
|
+
try {
|
|
1013
|
+
execFileSync("gh", ["auth", "status"], { timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1014
|
+
try {
|
|
1015
|
+
execFileSync("gh", ["api", "user/starred/tellang/triflux"], { timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
1016
|
+
console.log(` ${G}⭐${R} 이미 함께하고 계시군요.`);
|
|
1017
|
+
} catch {
|
|
1018
|
+
console.log(` ${Y}⭐${R} 하나가 큰 차이를 만듭니다. ${D}https://github.com/tellang/triflux${R}`);
|
|
1019
|
+
}
|
|
1020
|
+
} catch {
|
|
1021
|
+
// gh 미설치/미인증 — 무시
|
|
1022
|
+
}
|
|
903
1023
|
}
|
|
904
1024
|
|
|
905
1025
|
process.exit(0);
|