@triflux/core 10.50.1 → 10.51.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/hooks/agy-session-hook.mjs +11 -56
- package/hooks/codex-session-hook.mjs +11 -101
- package/hub/bridge.mjs +1 -79
- package/hub/cli-adapter-base.mjs +12 -90
- package/hub/codex-adapter.mjs +0 -1
- package/hub/codex-preflight.mjs +5 -28
- package/hub/index.mjs +3 -9
- package/hub/lib/cto-env.mjs +0 -23
- package/hub/lib/known-errors.json +2 -2
- package/hub/lib/process-utils.mjs +0 -123
- package/hub/lib/spawn-trace.mjs +3 -2
- package/hub/lib/timeout-defaults.mjs +0 -1
- package/hub/paths.mjs +0 -6
- package/hub/pipeline/index.mjs +0 -190
- package/hub/pipeline/transitions.mjs +0 -10
- package/hub/reflexion.mjs +1 -84
- package/hub/role-contract.mjs +0 -2
- package/hud/cli-policy.mjs +0 -26
- package/hud/colors.mjs +0 -13
- package/hud/constants.mjs +7 -153
- package/hud/context-monitor.mjs +3 -64
- package/hud/hud-qos-status.mjs +54 -264
- package/hud/providers/claude.mjs +4 -16
- package/hud/providers/gemini.mjs +83 -689
- package/hud/renderers.mjs +59 -367
- package/hud/terminal.mjs +1 -63
- package/hud/utils.mjs +2 -26
- package/package.json +1 -4
- package/scripts/lib/agent-route-policy.mjs +162 -14
- package/scripts/lib/claudemd-scanner.mjs +2 -199
- package/scripts/lib/cli-claude.mjs +0 -2
- package/scripts/lib/context.mjs +3 -19
- package/scripts/lib/env-probe.mjs +5 -64
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +25 -104
- package/scripts/lib/session-presence.mjs +1 -1
- package/scripts/lib/skill-template.mjs +0 -271
- package/hub/codex-compat.mjs +0 -12
- package/hub/dynamic-routing-engine.mjs +0 -511
- package/hub/fullcycle.mjs +0 -113
- package/hub/gemini-adapter.mjs +0 -174
- package/hub/intent.mjs +0 -321
- package/hub/lib/mcp-response-cache.mjs +0 -239
- package/hub/lib/path-utils.mjs +0 -165
- package/hub/lib/risk-tier.mjs +0 -53
- package/hub/lib/ssh-command.mjs +0 -231
- package/hub/lib/staged-file-detect.mjs +0 -43
- package/hub/lib/tfx-route-args.mjs +0 -353
- package/hub/lib/timeline-adapter.mjs +0 -190
- package/hub/pipeline/gates/confidence.mjs +0 -75
- package/hub/pipeline/gates/consensus.mjs +0 -106
- package/hub/pipeline/gates/index.mjs +0 -9
- package/hub/pipeline/gates/selfcheck.mjs +0 -120
- package/hub/quality/deslop.mjs +0 -274
- package/hub/research.mjs +0 -155
- package/hub/role-control-events.mjs +0 -113
- package/hub/routing/complexity.mjs +0 -269
- package/hub/routing/index.mjs +0 -122
- package/hub/routing/q-learning.mjs +0 -387
- package/hub/routing-snapshot.mjs +0 -261
- package/hub/token-mode.mjs +0 -228
- package/hud/mission-board.mjs +0 -56
- package/hud/providers/cto.mjs +0 -76
- package/mesh/index.mjs +0 -69
- package/mesh/mesh-budget.mjs +0 -137
- package/mesh/mesh-heartbeat.mjs +0 -100
- package/mesh/mesh-protocol.mjs +0 -96
- package/mesh/mesh-queue.mjs +0 -163
- package/mesh/mesh-registry.mjs +0 -78
- package/mesh/mesh-router.mjs +0 -77
- package/scripts/lib/agent-json.mjs +0 -27
- package/scripts/lib/async.mjs +0 -174
- package/scripts/lib/cli-agy.mjs +0 -85
- package/scripts/lib/cli-codex.mjs +0 -62
- package/scripts/lib/cli-gemini.mjs +0 -37
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/env.mjs +0 -14
- package/scripts/lib/hub.mjs +0 -48
- package/scripts/lib/pid.mjs +0 -63
- package/scripts/lib/quota.mjs +0 -47
- package/scripts/lib/skill-state.mjs +0 -220
- package/scripts/lib/team.mjs +0 -75
- package/scripts/lib/timeout.mjs +0 -66
- package/scripts/lib/tmp.mjs +0 -26
- package/scripts/lib/toml.mjs +0 -132
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
// hub/lib/mcp-response-cache.mjs — MCP 응답 캐시 (LRU + TTL)
|
|
2
|
-
// Deep 스킬에서 3개 모델이 같은 context7 문서를 요청하면 1번만 호출, 나머지는 캐시 hit.
|
|
3
|
-
// 파일 기반 영속 + 메모리 기반 핫캐시 이중 구조.
|
|
4
|
-
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
|
-
import {
|
|
7
|
-
existsSync,
|
|
8
|
-
mkdirSync,
|
|
9
|
-
readdirSync,
|
|
10
|
-
readFileSync,
|
|
11
|
-
unlinkSync,
|
|
12
|
-
writeFileSync,
|
|
13
|
-
} from "node:fs";
|
|
14
|
-
import { homedir } from "node:os";
|
|
15
|
-
import { join } from "node:path";
|
|
16
|
-
|
|
17
|
-
const DEFAULT_CACHE_DIR = join(homedir(), ".triflux", "mcp-cache");
|
|
18
|
-
const DEFAULT_MAX_ENTRIES = 500;
|
|
19
|
-
|
|
20
|
-
// 서버별 TTL (ms)
|
|
21
|
-
const SERVER_TTL = Object.freeze({
|
|
22
|
-
context7: 60 * 60 * 1000, // 1시간 — 라이브러리 문서는 자주 안 바뀜
|
|
23
|
-
"brave-search": 10 * 60 * 1000, // 10분 — 웹 검색
|
|
24
|
-
exa: 10 * 60 * 1000, // 10분
|
|
25
|
-
tavily: 10 * 60 * 1000, // 10분
|
|
26
|
-
serena: 5 * 60 * 1000, // 5분 — 코드 분석 (코드 변경 가능)
|
|
27
|
-
jira: 2 * 60 * 1000, // 2분 — 이슈 상태 자주 변경
|
|
28
|
-
notion: 5 * 60 * 1000, // 5분
|
|
29
|
-
_default: 5 * 60 * 1000, // 기본 5분
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
function hashKey(server, method, params) {
|
|
33
|
-
const payload = JSON.stringify({ server, method, params });
|
|
34
|
-
return createHash("sha256").update(payload).digest("hex").slice(0, 16);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* MCP 응답 캐시 생성.
|
|
39
|
-
* @param {object} [opts]
|
|
40
|
-
* @param {string} [opts.cacheDir] — 캐시 디렉토리
|
|
41
|
-
* @param {number} [opts.maxEntries] — 최대 엔트리 수 (LRU)
|
|
42
|
-
* @returns {McpResponseCache}
|
|
43
|
-
*/
|
|
44
|
-
export function createMcpResponseCache(opts = {}) {
|
|
45
|
-
const cacheDir = opts.cacheDir || DEFAULT_CACHE_DIR;
|
|
46
|
-
const maxEntries = opts.maxEntries || DEFAULT_MAX_ENTRIES;
|
|
47
|
-
|
|
48
|
-
mkdirSync(cacheDir, { recursive: true });
|
|
49
|
-
|
|
50
|
-
// 메모리 핫캐시 (Map — insertion order = LRU)
|
|
51
|
-
const hot = new Map();
|
|
52
|
-
|
|
53
|
-
let hits = 0;
|
|
54
|
-
let misses = 0;
|
|
55
|
-
|
|
56
|
-
function getTtl(server) {
|
|
57
|
-
return SERVER_TTL[server] || SERVER_TTL._default;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function getCachePath(key) {
|
|
61
|
-
return join(cacheDir, `${key}.json`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* 캐시 조회.
|
|
66
|
-
* @param {string} server — MCP 서버 이름
|
|
67
|
-
* @param {string} method — MCP 메서드
|
|
68
|
-
* @param {*} params — 요청 파라미터
|
|
69
|
-
* @returns {{ hit: boolean, data?: * }}
|
|
70
|
-
*/
|
|
71
|
-
function get(server, method, params) {
|
|
72
|
-
const key = hashKey(server, method, params);
|
|
73
|
-
const ttl = getTtl(server);
|
|
74
|
-
|
|
75
|
-
// 1) 메모리 핫캐시
|
|
76
|
-
if (hot.has(key)) {
|
|
77
|
-
const entry = hot.get(key);
|
|
78
|
-
if (Date.now() - entry.ts < ttl) {
|
|
79
|
-
hits++;
|
|
80
|
-
// LRU: 삭제 후 재삽입 (Map은 insertion order)
|
|
81
|
-
hot.delete(key);
|
|
82
|
-
hot.set(key, entry);
|
|
83
|
-
return { hit: true, data: entry.data };
|
|
84
|
-
}
|
|
85
|
-
hot.delete(key);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// 2) 파일 캐시
|
|
89
|
-
const path = getCachePath(key);
|
|
90
|
-
if (existsSync(path)) {
|
|
91
|
-
try {
|
|
92
|
-
const entry = JSON.parse(readFileSync(path, "utf8"));
|
|
93
|
-
if (Date.now() - entry.ts < ttl) {
|
|
94
|
-
hits++;
|
|
95
|
-
hot.set(key, entry);
|
|
96
|
-
evictIfNeeded();
|
|
97
|
-
return { hit: true, data: entry.data };
|
|
98
|
-
}
|
|
99
|
-
// expired — 삭제
|
|
100
|
-
try {
|
|
101
|
-
unlinkSync(path);
|
|
102
|
-
} catch {
|
|
103
|
-
/* ignore */
|
|
104
|
-
}
|
|
105
|
-
} catch {
|
|
106
|
-
/* corrupt file */
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
misses++;
|
|
111
|
-
return { hit: false };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* 캐시 저장.
|
|
116
|
-
* @param {string} server
|
|
117
|
-
* @param {string} method
|
|
118
|
-
* @param {*} params
|
|
119
|
-
* @param {*} data — 응답 데이터
|
|
120
|
-
*/
|
|
121
|
-
function set(server, method, params, data) {
|
|
122
|
-
const key = hashKey(server, method, params);
|
|
123
|
-
const entry = { ts: Date.now(), server, method, data };
|
|
124
|
-
|
|
125
|
-
// 메모리
|
|
126
|
-
hot.set(key, entry);
|
|
127
|
-
evictIfNeeded();
|
|
128
|
-
|
|
129
|
-
// 파일
|
|
130
|
-
try {
|
|
131
|
-
writeFileSync(getCachePath(key), JSON.stringify(entry), "utf8");
|
|
132
|
-
} catch {
|
|
133
|
-
/* best-effort */
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function evictIfNeeded() {
|
|
138
|
-
while (hot.size > maxEntries) {
|
|
139
|
-
const oldest = hot.keys().next().value;
|
|
140
|
-
hot.delete(oldest);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* 만료된 파일 캐시 정리.
|
|
146
|
-
* @returns {number} 삭제된 파일 수
|
|
147
|
-
*/
|
|
148
|
-
function prune() {
|
|
149
|
-
let pruned = 0;
|
|
150
|
-
try {
|
|
151
|
-
for (const name of readdirSync(cacheDir)) {
|
|
152
|
-
if (!name.endsWith(".json")) continue;
|
|
153
|
-
const path = join(cacheDir, name);
|
|
154
|
-
try {
|
|
155
|
-
const entry = JSON.parse(readFileSync(path, "utf8"));
|
|
156
|
-
const ttl = getTtl(entry.server || "_default");
|
|
157
|
-
if (Date.now() - entry.ts >= ttl) {
|
|
158
|
-
unlinkSync(path);
|
|
159
|
-
pruned++;
|
|
160
|
-
}
|
|
161
|
-
} catch {
|
|
162
|
-
// corrupt — 삭제
|
|
163
|
-
try {
|
|
164
|
-
unlinkSync(path);
|
|
165
|
-
pruned++;
|
|
166
|
-
} catch {
|
|
167
|
-
/* ignore */
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
} catch {
|
|
172
|
-
/* ignore */
|
|
173
|
-
}
|
|
174
|
-
return pruned;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* 캐시 통계.
|
|
179
|
-
* @returns {{ hits, misses, hotSize, diskFiles, hitRate }}
|
|
180
|
-
*/
|
|
181
|
-
function stats() {
|
|
182
|
-
let diskFiles = 0;
|
|
183
|
-
try {
|
|
184
|
-
diskFiles = readdirSync(cacheDir).filter((n) =>
|
|
185
|
-
n.endsWith(".json"),
|
|
186
|
-
).length;
|
|
187
|
-
} catch {
|
|
188
|
-
/* ignore */
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const total = hits + misses;
|
|
192
|
-
return {
|
|
193
|
-
hits,
|
|
194
|
-
misses,
|
|
195
|
-
hotSize: hot.size,
|
|
196
|
-
diskFiles,
|
|
197
|
-
hitRate: total > 0 ? `${((hits / total) * 100).toFixed(1)}%` : "0%",
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/** 전체 캐시 초기화. */
|
|
202
|
-
function clear() {
|
|
203
|
-
hot.clear();
|
|
204
|
-
try {
|
|
205
|
-
for (const name of readdirSync(cacheDir)) {
|
|
206
|
-
if (name.endsWith(".json")) {
|
|
207
|
-
try {
|
|
208
|
-
unlinkSync(join(cacheDir, name));
|
|
209
|
-
} catch {
|
|
210
|
-
/* ignore */
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
} catch {
|
|
215
|
-
/* ignore */
|
|
216
|
-
}
|
|
217
|
-
hits = 0;
|
|
218
|
-
misses = 0;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return Object.freeze({
|
|
222
|
-
get,
|
|
223
|
-
set,
|
|
224
|
-
prune,
|
|
225
|
-
stats,
|
|
226
|
-
clear,
|
|
227
|
-
get cacheDir() {
|
|
228
|
-
return cacheDir;
|
|
229
|
-
},
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// 싱글톤 인스턴스 (Hub 프로세스 내에서 공유)
|
|
234
|
-
let _instance = null;
|
|
235
|
-
|
|
236
|
-
export function getMcpResponseCache(opts) {
|
|
237
|
-
if (!_instance) _instance = createMcpResponseCache(opts);
|
|
238
|
-
return _instance;
|
|
239
|
-
}
|
package/hub/lib/path-utils.mjs
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
// hub/lib/path-utils.mjs — Windows/POSIX 경로 변환 유틸리티
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Windows 경로를 Git Bash 스타일 POSIX 경로로 변환한다.
|
|
5
|
-
* C:\foo\bar → /c/foo/bar
|
|
6
|
-
* 이미 POSIX 경로면 그대로 반환.
|
|
7
|
-
* null/undefined → 빈 문자열
|
|
8
|
-
* @param {string|null|undefined} windowsPath
|
|
9
|
-
* @returns {string}
|
|
10
|
-
*/
|
|
11
|
-
export function toPosixPath(windowsPath) {
|
|
12
|
-
if (windowsPath == null) return "";
|
|
13
|
-
const p = String(windowsPath);
|
|
14
|
-
if (!p) return "";
|
|
15
|
-
|
|
16
|
-
// 이미 POSIX 경로 (/ 로 시작하거나 드라이브 레터 없음)
|
|
17
|
-
const winDriveMatch = p.match(/^([A-Za-z]):[/\\](.*)/);
|
|
18
|
-
if (!winDriveMatch) {
|
|
19
|
-
// 백슬래시만 있는 상대 경로도 forward slash로 변환
|
|
20
|
-
return p.replace(/\\/g, "/");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const drive = winDriveMatch[1].toLowerCase();
|
|
24
|
-
const rest = winDriveMatch[2].replace(/\\/g, "/");
|
|
25
|
-
return `/${drive}/${rest}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* POSIX 경로(Git Bash 스타일)를 Windows 경로로 변환한다.
|
|
30
|
-
* /c/foo/bar → C:\foo\bar
|
|
31
|
-
* 이미 Windows 경로면 그대로 반환.
|
|
32
|
-
* null/undefined → 빈 문자열
|
|
33
|
-
* @param {string|null|undefined} posixPath
|
|
34
|
-
* @returns {string}
|
|
35
|
-
*/
|
|
36
|
-
export function toWindowsPath(posixPath) {
|
|
37
|
-
if (posixPath == null) return "";
|
|
38
|
-
const p = String(posixPath);
|
|
39
|
-
if (!p) return "";
|
|
40
|
-
|
|
41
|
-
// 이미 Windows 경로
|
|
42
|
-
if (/^[A-Za-z]:/.test(p)) {
|
|
43
|
-
return p.replace(/\//g, "\\");
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Git Bash 스타일: /c/foo/bar
|
|
47
|
-
const gitBashMatch = p.match(/^\/([a-zA-Z])(\/.*)?$/);
|
|
48
|
-
if (gitBashMatch) {
|
|
49
|
-
const drive = gitBashMatch[1].toUpperCase();
|
|
50
|
-
const rest = gitBashMatch[2] ? gitBashMatch[2].replace(/\//g, "\\") : "\\";
|
|
51
|
-
return `${drive}:${rest}`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return p;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* 현재 OS에 맞게 경로를 정규화한다.
|
|
59
|
-
* win32: backslash, 그 외: forward slash
|
|
60
|
-
* @param {string|null|undefined} p
|
|
61
|
-
* @returns {string}
|
|
62
|
-
*/
|
|
63
|
-
export function normalizePath(p) {
|
|
64
|
-
if (p == null) return "";
|
|
65
|
-
const str = String(p);
|
|
66
|
-
if (process.platform === "win32") {
|
|
67
|
-
return str.replace(/\//g, "\\");
|
|
68
|
-
}
|
|
69
|
-
return str.replace(/\\/g, "/");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* 쉘 타입에 맞는 경로로 변환한다.
|
|
74
|
-
* @param {string|null|undefined} path
|
|
75
|
-
* @param {'git-bash'|'wsl'|'cmd'|'powershell'} shellType
|
|
76
|
-
* @returns {string}
|
|
77
|
-
*/
|
|
78
|
-
export function resolveShellPath(path, shellType) {
|
|
79
|
-
if (path == null) return "";
|
|
80
|
-
const p = String(path);
|
|
81
|
-
|
|
82
|
-
switch (shellType) {
|
|
83
|
-
case "git-bash":
|
|
84
|
-
return toPosixPath(p);
|
|
85
|
-
|
|
86
|
-
case "wsl": {
|
|
87
|
-
// Git Bash 경로를 먼저 Windows로 변환 후 WSL 형식으로
|
|
88
|
-
const winPath = /^[A-Za-z]:/.test(p) ? p : toWindowsPath(p);
|
|
89
|
-
const wslDriveMatch = winPath.match(/^([A-Za-z]):[/\\](.*)/);
|
|
90
|
-
if (wslDriveMatch) {
|
|
91
|
-
const drive = wslDriveMatch[1].toLowerCase();
|
|
92
|
-
const rest = wslDriveMatch[2].replace(/\\/g, "/");
|
|
93
|
-
return rest ? `/mnt/${drive}/${rest}` : `/mnt/${drive}`;
|
|
94
|
-
}
|
|
95
|
-
// 이미 /mnt/ 형식이면 그대로
|
|
96
|
-
if (p.startsWith("/mnt/")) return p;
|
|
97
|
-
return p.replace(/\\/g, "/");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
case "cmd":
|
|
101
|
-
case "powershell":
|
|
102
|
-
return toWindowsPath(p);
|
|
103
|
-
|
|
104
|
-
default:
|
|
105
|
-
return p;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* 환경 변수와 플랫폼 정보로 현재 쉘 타입을 추론한다.
|
|
111
|
-
* @returns {'git-bash'|'wsl'|'cmd'|'powershell'|'unix'}
|
|
112
|
-
*/
|
|
113
|
-
export function detectShellType() {
|
|
114
|
-
// WSL 감지: WSL_DISTRO_NAME 또는 WSLENV
|
|
115
|
-
if (process.env.WSL_DISTRO_NAME || process.env.WSLENV) {
|
|
116
|
-
return "wsl";
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Windows 플랫폼
|
|
120
|
-
if (process.platform === "win32") {
|
|
121
|
-
const shell = process.env.SHELL || "";
|
|
122
|
-
const term = process.env.TERM || "";
|
|
123
|
-
const msystem = process.env.MSYSTEM || "";
|
|
124
|
-
|
|
125
|
-
// Git Bash 감지: SHELL=/usr/bin/bash + MSYSTEM=MINGW64 등
|
|
126
|
-
if (
|
|
127
|
-
shell.includes("bash") ||
|
|
128
|
-
msystem.startsWith("MINGW") ||
|
|
129
|
-
msystem.startsWith("MSYS") ||
|
|
130
|
-
term === "xterm"
|
|
131
|
-
) {
|
|
132
|
-
return "git-bash";
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// PowerShell 감지
|
|
136
|
-
if (process.env.PSModulePath || process.env.PSHOME) {
|
|
137
|
-
return "powershell";
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return "cmd";
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// 비-Windows
|
|
144
|
-
return "unix";
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* WSL 경로 여부를 확인한다 (/mnt/ 시작).
|
|
149
|
-
* @param {string|null|undefined} p
|
|
150
|
-
* @returns {boolean}
|
|
151
|
-
*/
|
|
152
|
-
export function isWslPath(p) {
|
|
153
|
-
if (p == null) return false;
|
|
154
|
-
return String(p).startsWith("/mnt/");
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Git Bash 경로 여부를 확인한다 (/c/ 또는 /d/ 등 단일 소문자 드라이브 레터).
|
|
159
|
-
* @param {string|null|undefined} p
|
|
160
|
-
* @returns {boolean}
|
|
161
|
-
*/
|
|
162
|
-
export function isGitBashPath(p) {
|
|
163
|
-
if (p == null) return false;
|
|
164
|
-
return /^\/[a-zA-Z](\/|$)/.test(String(p));
|
|
165
|
-
}
|
package/hub/lib/risk-tier.mjs
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
export const HIGH_PATTERNS = Object.freeze([
|
|
2
|
-
/^hub\//,
|
|
3
|
-
/^scripts\//,
|
|
4
|
-
/^\.claude\/rules\//,
|
|
5
|
-
/^bin\//,
|
|
6
|
-
/^\.github\//,
|
|
7
|
-
]);
|
|
8
|
-
|
|
9
|
-
export const MEDIUM_PATTERNS = Object.freeze([
|
|
10
|
-
/package\.json$/,
|
|
11
|
-
/\.ya?ml$/,
|
|
12
|
-
/\.toml$/,
|
|
13
|
-
/^config\//,
|
|
14
|
-
/^hooks\//,
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
|
-
export const LOW_PATTERNS = Object.freeze([/\.md$/, /\.txt$/, /\.test\./]);
|
|
18
|
-
|
|
19
|
-
export function classifyRiskTier({ changedFiles }) {
|
|
20
|
-
const files = Array.isArray(changedFiles)
|
|
21
|
-
? changedFiles.filter((file) => typeof file === "string" && file.length > 0)
|
|
22
|
-
: [];
|
|
23
|
-
|
|
24
|
-
if (files.length === 0) {
|
|
25
|
-
return "low";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (matchesAny(files, HIGH_PATTERNS)) {
|
|
29
|
-
return "high";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (files.length > 1) {
|
|
33
|
-
return "medium";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
if (matchesAny(files, MEDIUM_PATTERNS)) {
|
|
37
|
-
return "medium";
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (matchesPattern(files[0], LOW_PATTERNS)) {
|
|
41
|
-
return "low";
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return "medium";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function matchesAny(files, patterns) {
|
|
48
|
-
return files.some((file) => matchesPattern(file, patterns));
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function matchesPattern(file, patterns) {
|
|
52
|
-
return patterns.some((pattern) => pattern.test(file));
|
|
53
|
-
}
|
package/hub/lib/ssh-command.mjs
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
// hub/lib/ssh-command.mjs — OS-aware SSH command builder
|
|
2
|
-
// PowerShell 호스트에 bash 문법(2>/dev/null, &&, $())을 보내는 사고를 방지한다.
|
|
3
|
-
// 모든 SSH 명령 생성 코드에서 이 유틸리티를 사용할 것.
|
|
4
|
-
|
|
5
|
-
import { readHost, readHosts, resolveHost } from "./hosts-compat.mjs";
|
|
6
|
-
|
|
7
|
-
/** hosts.json 캐시 (프로세스 수명 동안 유지) */
|
|
8
|
-
let hostsCache = null;
|
|
9
|
-
let hostsCacheRoot = null;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* hosts.json을 로드하여 캐시한다.
|
|
13
|
-
* @param {string} [repoRoot] — 프로젝트 루트 경로
|
|
14
|
-
*/
|
|
15
|
-
function loadHostsCache(repoRoot) {
|
|
16
|
-
const cacheKey = repoRoot || process.cwd();
|
|
17
|
-
if (hostsCache && hostsCacheRoot === cacheKey) return;
|
|
18
|
-
hostsCache = readHosts(repoRoot);
|
|
19
|
-
hostsCacheRoot = cacheKey;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* hosts.json에서 호스트 OS를 조회한다.
|
|
24
|
-
* @param {string} hostAlias — hosts.json의 키 (e.g. "ultra4")
|
|
25
|
-
* @param {string} [repoRoot] — 프로젝트 루트 경로
|
|
26
|
-
* @returns {"windows"|"posix"} 호스트 OS 유형
|
|
27
|
-
*/
|
|
28
|
-
export function detectHostOs(hostAlias, repoRoot) {
|
|
29
|
-
loadHostsCache(repoRoot);
|
|
30
|
-
|
|
31
|
-
const hostCfg = resolveHost(hostAlias, repoRoot)?.host;
|
|
32
|
-
if (hostCfg?.os === "windows") return "windows";
|
|
33
|
-
if (hostCfg?.os) return "posix";
|
|
34
|
-
|
|
35
|
-
// IP 주소나 unknown alias → posix 기본값
|
|
36
|
-
return "posix";
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* OS에 맞는 셸 쿼팅을 적용한다.
|
|
41
|
-
* - posix: 싱글쿼트 래핑 + 내부 싱글쿼트 이스케이프
|
|
42
|
-
* - windows: PowerShell 싱글쿼트 래핑 + 내부 싱글쿼트 더블링
|
|
43
|
-
* @param {string} value
|
|
44
|
-
* @param {"windows"|"posix"} os
|
|
45
|
-
* @returns {string}
|
|
46
|
-
*/
|
|
47
|
-
export function shellQuoteForHost(value, os) {
|
|
48
|
-
const s = String(value);
|
|
49
|
-
if (os === "windows") {
|
|
50
|
-
// PowerShell: 싱글쿼트 내부에서 '' 로 이스케이프
|
|
51
|
-
return `'${s.replace(/'/g, "''")}'`;
|
|
52
|
-
}
|
|
53
|
-
// POSIX: 싱글쿼트 내부에서 '\'' 로 이스케이프
|
|
54
|
-
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* OS에 맞는 stderr 억제 구문을 반환한다.
|
|
59
|
-
* @param {"windows"|"posix"} os
|
|
60
|
-
* @returns {string} "2>$null" 또는 "2>/dev/null"
|
|
61
|
-
*/
|
|
62
|
-
export function suppressStderr(os) {
|
|
63
|
-
return os === "windows" ? "2>$null" : "2>/dev/null";
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* OS에 맞는 명령 연결자를 반환한다.
|
|
68
|
-
* @param {"windows"|"posix"} os
|
|
69
|
-
* @returns {string} "; " (PowerShell) 또는 " && " (bash)
|
|
70
|
-
*/
|
|
71
|
-
export function commandJoin(os) {
|
|
72
|
-
return os === "windows" ? "; " : " && ";
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* OS에 맞는 null 디바이스 경로를 반환한다.
|
|
77
|
-
* @param {"windows"|"posix"} os
|
|
78
|
-
* @returns {string}
|
|
79
|
-
*/
|
|
80
|
-
export function nullDevice(os) {
|
|
81
|
-
return os === "windows" ? "$null" : "/dev/null";
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* SSH execFileSync 인자 배열을 안전하게 구성한다.
|
|
86
|
-
* psmux 명령처럼 원격 셸에서 실행할 명령을 배열로 받아
|
|
87
|
-
* 적절한 쿼팅을 적용한 SSH 인자 배열을 반환한다.
|
|
88
|
-
*
|
|
89
|
-
* @param {string} host — SSH 호스트 (user@ip 또는 alias)
|
|
90
|
-
* @param {string[]} remoteCmd — 원격에서 실행할 명령 + 인자
|
|
91
|
-
* @param {object} [opts]
|
|
92
|
-
* @param {number} [opts.connectTimeout=5] — SSH 연결 타임아웃(초)
|
|
93
|
-
* @param {boolean} [opts.keepalive=true] — ServerAliveInterval/CountMax 활성화
|
|
94
|
-
* @param {"windows"|"posix"} [opts.os] — 호스트 OS (미지정 시 자동 감지)
|
|
95
|
-
* @param {string} [opts.repoRoot] — hosts.json 탐색 경로
|
|
96
|
-
* @returns {string[]} execFileSync('ssh', returnValue) 형태로 사용
|
|
97
|
-
*/
|
|
98
|
-
export function buildSshArgs(host, remoteCmd, opts = {}) {
|
|
99
|
-
const connectTimeout = opts.connectTimeout ?? 5;
|
|
100
|
-
const keepalive = opts.keepalive !== false;
|
|
101
|
-
const hostAlias = host.includes("@") ? host.split("@").pop() : host;
|
|
102
|
-
const os = opts.os ?? detectHostOs(hostAlias, opts.repoRoot);
|
|
103
|
-
|
|
104
|
-
// 명령어(첫 요소)는 쿼팅하지 않고, 인자만 쿼팅
|
|
105
|
-
const [cmd, ...args] = remoteCmd;
|
|
106
|
-
const quotedArgs = args.map((a) => shellQuoteForHost(a, os));
|
|
107
|
-
const remoteCmdStr = [cmd, ...quotedArgs].join(" ");
|
|
108
|
-
|
|
109
|
-
const sshArgs = [
|
|
110
|
-
"-o",
|
|
111
|
-
`ConnectTimeout=${connectTimeout}`,
|
|
112
|
-
"-o",
|
|
113
|
-
"BatchMode=yes",
|
|
114
|
-
];
|
|
115
|
-
|
|
116
|
-
if (keepalive) {
|
|
117
|
-
sshArgs.push("-o", "ServerAliveInterval=30", "-o", "ServerAliveCountMax=3");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
sshArgs.push(host, remoteCmdStr);
|
|
121
|
-
return sshArgs;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* 검증: 문자열에 호스트 OS와 맞지 않는 셸 문법이 포함되었는지 검사한다.
|
|
126
|
-
* SSH 명령 생성 시 안전 게이트로 사용.
|
|
127
|
-
*
|
|
128
|
-
* @param {string} command — 원격 실행할 명령 문자열
|
|
129
|
-
* @param {"windows"|"posix"} os — 대상 호스트 OS
|
|
130
|
-
* @returns {{ safe: boolean, violations: string[] }}
|
|
131
|
-
*/
|
|
132
|
-
export function validateCommandForOs(command, os) {
|
|
133
|
-
const violations = [];
|
|
134
|
-
|
|
135
|
-
if (os === "windows") {
|
|
136
|
-
// PowerShell에서 해석 불가능한 bash 문법 검출
|
|
137
|
-
if (/2>\/dev\/null/.test(command)) {
|
|
138
|
-
violations.push("2>/dev/null → PowerShell에서는 2>$null 사용");
|
|
139
|
-
}
|
|
140
|
-
if (/\$\(/.test(command) && !/\$\(/.test(command) === false) {
|
|
141
|
-
// $() bash substitution vs PowerShell $() — 문맥 의존
|
|
142
|
-
// PowerShell도 $()를 사용하므로 여기선 경고만
|
|
143
|
-
}
|
|
144
|
-
if (/\s&&\s/.test(command)) {
|
|
145
|
-
violations.push("&& → PowerShell에서는 ; 또는 -and 사용");
|
|
146
|
-
}
|
|
147
|
-
if (/\s\|\|\s/.test(command)) {
|
|
148
|
-
violations.push("|| → PowerShell에서는 ; 또는 -or 사용");
|
|
149
|
-
}
|
|
150
|
-
if (/>\s*\/dev\/null/.test(command)) {
|
|
151
|
-
violations.push(">/dev/null → PowerShell에서는 >$null 사용");
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return { safe: violations.length === 0, violations };
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* hosts.json에서 호스트 전체 설정을 조회한다.
|
|
160
|
-
* @param {string} hostAlias — hosts.json의 키 (e.g. "ultra4")
|
|
161
|
-
* @param {string} [repoRoot]
|
|
162
|
-
* @returns {object|null}
|
|
163
|
-
*/
|
|
164
|
-
export function getHostConfig(hostAlias, repoRoot) {
|
|
165
|
-
loadHostsCache(repoRoot);
|
|
166
|
-
return readHost(hostAlias, repoRoot);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* 별칭(alias)으로 호스트 키를 찾는다.
|
|
171
|
-
* @param {string} alias — 호스트 키 또는 aliases 배열 내 값
|
|
172
|
-
* @param {string} [repoRoot]
|
|
173
|
-
* @returns {string|null} 호스트 키 (e.g. "ultra4") 또는 null
|
|
174
|
-
*/
|
|
175
|
-
export function resolveHostAlias(alias, repoRoot) {
|
|
176
|
-
loadHostsCache(repoRoot);
|
|
177
|
-
|
|
178
|
-
return resolveHost(alias, repoRoot)?.name ?? null;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* 특정 capability를 보유한 호스트 목록을 반환한다.
|
|
183
|
-
* 명시적 선택 UI에서 바로 리소스 정보를 보여줄 수 있도록 specs를 평탄화한다.
|
|
184
|
-
* @param {string} capability — e.g. "codex", "claude", "gpu"
|
|
185
|
-
* @param {string} [repoRoot]
|
|
186
|
-
* @returns {Array<{ name: string, config: object, specs: { cores?: number, ram_gb?: number } }>}
|
|
187
|
-
*/
|
|
188
|
-
export function selectHostForCapability(capability, repoRoot) {
|
|
189
|
-
loadHostsCache(repoRoot);
|
|
190
|
-
|
|
191
|
-
const matches = [];
|
|
192
|
-
for (const [name, cfg] of Object.entries(hostsCache.hosts || {})) {
|
|
193
|
-
if (cfg.capabilities?.includes(capability)) {
|
|
194
|
-
matches.push({
|
|
195
|
-
name,
|
|
196
|
-
config: cfg,
|
|
197
|
-
specs: {
|
|
198
|
-
cores: cfg.specs?.cores,
|
|
199
|
-
ram_gb: cfg.specs?.ram_gb,
|
|
200
|
-
},
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
return matches;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* 특정 capability를 가진 호스트를 리소스 기준으로 정렬해 반환한다.
|
|
209
|
-
* 자동 선택은 하지 않고, 명시적 선택 목록 정렬에만 사용한다.
|
|
210
|
-
* @param {string} capability — e.g. "codex", "claude", "gpu"
|
|
211
|
-
* @param {"cores"|"ram_gb"} sortBy
|
|
212
|
-
* @param {string} [repoRoot]
|
|
213
|
-
* @returns {Array<{ name: string, config: object, specs: { cores?: number, ram_gb?: number } }>}
|
|
214
|
-
*/
|
|
215
|
-
export function selectHostByResources(capability, sortBy, repoRoot) {
|
|
216
|
-
if (sortBy !== "cores" && sortBy !== "ram_gb") {
|
|
217
|
-
throw new TypeError('sortBy must be "cores" or "ram_gb"');
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
return selectHostForCapability(capability, repoRoot).sort((a, b) => {
|
|
221
|
-
const aValue = a.specs?.[sortBy] ?? -Infinity;
|
|
222
|
-
const bValue = b.specs?.[sortBy] ?? -Infinity;
|
|
223
|
-
return bValue - aValue;
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/** hosts.json 캐시 초기화 (테스트용) */
|
|
228
|
-
export function resetHostsCache() {
|
|
229
|
-
hostsCache = null;
|
|
230
|
-
hostsCacheRoot = null;
|
|
231
|
-
}
|