@triflux/core 10.0.3 → 10.25.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/hooks/agent-route-guard.mjs +14 -10
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +47 -7
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +279 -33
- package/hooks/hook-registry.json +104 -7
- package/hooks/hooks.json +49 -1
- package/hooks/keyword-rules.json +183 -23
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +369 -22
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/safety-guard.mjs +308 -18
- package/hooks/session-end-cleanup.mjs +345 -0
- package/hooks/session-start-fast.mjs +214 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +9 -4
- package/hub/account-broker.mjs +1036 -66
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +96 -58
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +163 -126
- package/hub/bridge.mjs +832 -291
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -3
- package/hub/codex-preflight.mjs +113 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/dynamic-routing-engine.mjs +511 -0
- package/hub/fullcycle.mjs +34 -17
- package/hub/gemini-adapter.mjs +77 -95
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +171 -39
- package/hub/lib/bash-path.mjs +73 -0
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/env-detect.mjs +145 -0
- package/hub/lib/hosts-compat.mjs +319 -0
- package/hub/lib/mcp-response-cache.mjs +239 -0
- package/hub/lib/memory-store.mjs +303 -107
- package/hub/lib/path-utils.mjs +165 -0
- package/hub/lib/phase-manager.mjs +493 -0
- package/hub/lib/process-utils.mjs +797 -104
- package/hub/lib/prompt-tmp.mjs +33 -0
- package/hub/lib/risk-tier.mjs +53 -0
- package/hub/lib/spawn-trace.mjs +561 -0
- package/hub/lib/ssh-command.mjs +44 -24
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/staged-file-detect.mjs +43 -0
- package/hub/lib/state-snapshot.mjs +293 -0
- package/hub/lib/tfx-route-args.mjs +309 -0
- package/hub/lib/timeline-adapter.mjs +190 -0
- package/hub/lib/trace-recorder.mjs +153 -0
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/quota-middleware.mjs +70 -0
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +65 -48
- package/hub/quality/deslop.mjs +70 -49
- package/hub/reflexion.mjs +116 -58
- package/hub/research.mjs +34 -25
- package/hub/router.mjs +941 -791
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +76 -28
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +129 -63
- package/hub/state.mjs +86 -44
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/token-mode.mjs +76 -72
- package/hub/workers/worker-utils.mjs +22 -16
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +184 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +153 -50
- package/hud/mission-board.mjs +56 -0
- package/hud/providers/claude.mjs +398 -114
- package/hud/providers/codex.mjs +96 -21
- package/hud/providers/gemini.mjs +184 -51
- package/hud/renderers.mjs +413 -123
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +81 -39
- package/mesh/index.mjs +69 -0
- package/mesh/mesh-budget.mjs +137 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +163 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +77 -0
- package/package.json +5 -1
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +13 -5
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +58 -30
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +48 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +393 -249
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +1282 -116
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +24 -14
- package/scripts/lib/mcp-server-catalog.mjs +355 -118
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +72 -12
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +36 -11
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
package/hub/intent.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// hub/intent.mjs — Intent Classification Engine
|
|
2
2
|
// 사용자 요청의 "진짜 의도"를 분석 → 카테고리 분류 → 최적 에이전트/모델 자동 선택
|
|
3
3
|
|
|
4
|
-
import { execFileSync } from
|
|
5
|
-
import crypto from
|
|
6
|
-
import { whichCommand } from
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import { whichCommand } from "./platform.mjs";
|
|
7
7
|
|
|
8
8
|
/** 캐시 엔트리: { category, confidence, ts } */
|
|
9
9
|
const _intentCache = new Map();
|
|
@@ -12,14 +12,15 @@ const CACHE_TTL_MS = 5 * 60 * 1000; // 5분
|
|
|
12
12
|
/** codex 설치 여부 (프로세스당 1회 확인) */
|
|
13
13
|
let _codexAvailable = null;
|
|
14
14
|
|
|
15
|
+
/** @experimental 런타임 미연결 — 향후 통합 예정 */
|
|
15
16
|
function _isCodexAvailable() {
|
|
16
17
|
if (_codexAvailable !== null) return _codexAvailable;
|
|
17
|
-
_codexAvailable = Boolean(whichCommand(
|
|
18
|
+
_codexAvailable = Boolean(whichCommand("codex"));
|
|
18
19
|
return _codexAvailable;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
function _promptHash(prompt) {
|
|
22
|
-
return crypto.createHash(
|
|
23
|
+
return crypto.createHash("md5").update(prompt).digest("hex");
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
function _getCached(hash) {
|
|
@@ -35,16 +36,17 @@ function _getCached(hash) {
|
|
|
35
36
|
function _tryCodexClassify(prompt) {
|
|
36
37
|
try {
|
|
37
38
|
const raw = execFileSync(
|
|
38
|
-
|
|
39
|
-
[
|
|
40
|
-
{ timeout: 8000, encoding:
|
|
39
|
+
"codex",
|
|
40
|
+
["exec", `Classify intent: ${prompt}. Reply JSON: {intent, confidence}`],
|
|
41
|
+
{ timeout: 8000, encoding: "utf8" },
|
|
41
42
|
);
|
|
42
43
|
// JSON 블록 추출 (응답에 다른 텍스트가 섞일 수 있음)
|
|
43
44
|
const match = raw.match(/\{[\s\S]*?\}/);
|
|
44
45
|
if (!match) return null;
|
|
45
46
|
const parsed = JSON.parse(match[0]);
|
|
46
|
-
const intent = typeof parsed.intent ===
|
|
47
|
-
const confidence =
|
|
47
|
+
const intent = typeof parsed.intent === "string" ? parsed.intent : null;
|
|
48
|
+
const confidence =
|
|
49
|
+
typeof parsed.confidence === "number" ? parsed.confidence : null;
|
|
48
50
|
if (!intent || confidence === null) return null;
|
|
49
51
|
// intent가 알려진 카테고리여야 함
|
|
50
52
|
if (!INTENT_CATEGORIES[intent]) return null;
|
|
@@ -54,32 +56,154 @@ function _tryCodexClassify(prompt) {
|
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* @experimental
|
|
61
|
+
* triflux 특화 의도 카테고리 (10개)
|
|
62
|
+
*/
|
|
58
63
|
export const INTENT_CATEGORIES = {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
// 모델 정책 (2026-04-25):
|
|
65
|
+
// - gpt-5.5 = 메인 (코드 포함 모든 메인 직무, fast tier 가능)
|
|
66
|
+
// - gpt-5.4-mini = 자잘/부가/가성비 (fast tier 가능)
|
|
67
|
+
// - gpt-5.3-codex = escalation 가성비 중간 (fast 미지원)
|
|
68
|
+
// - gpt-5.4 폐기, gpt-5.5 로 격상
|
|
69
|
+
implement: { agent: "executor", mcp: "implement", effort: "gpt55_high" },
|
|
70
|
+
debug: { agent: "debugger", mcp: "implement", effort: "gpt55_xhigh" },
|
|
71
|
+
analyze: { agent: "analyst", mcp: "analyze", effort: "gpt55_xhigh" },
|
|
72
|
+
design: { agent: "architect", mcp: "analyze", effort: "gpt55_xhigh" },
|
|
73
|
+
review: { agent: "code-reviewer", mcp: "review", effort: "gpt55_high" },
|
|
74
|
+
document: { agent: "writer", mcp: "docs", effort: "pro" },
|
|
75
|
+
research: { agent: "scientist", mcp: "analyze", effort: "gpt55_high" },
|
|
76
|
+
"quick-fix": {
|
|
77
|
+
agent: "build-fixer",
|
|
78
|
+
mcp: "implement",
|
|
79
|
+
effort: "gpt55_low",
|
|
80
|
+
},
|
|
81
|
+
explain: { agent: "writer", mcp: "docs", effort: "flash" },
|
|
82
|
+
test: { agent: "test-engineer", mcp: null, effort: null },
|
|
69
83
|
};
|
|
70
84
|
|
|
71
85
|
/** @internal 키워드 → 카테고리 매핑 패턴 */
|
|
72
86
|
const KEYWORD_PATTERNS = [
|
|
73
|
-
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
{
|
|
88
|
+
category: "implement",
|
|
89
|
+
keywords: [
|
|
90
|
+
"구현",
|
|
91
|
+
"만들",
|
|
92
|
+
"추가",
|
|
93
|
+
"생성",
|
|
94
|
+
"작성",
|
|
95
|
+
"빌드",
|
|
96
|
+
"implement",
|
|
97
|
+
"create",
|
|
98
|
+
"add",
|
|
99
|
+
"build",
|
|
100
|
+
"make",
|
|
101
|
+
"develop",
|
|
102
|
+
],
|
|
103
|
+
weight: 1.0,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
category: "debug",
|
|
107
|
+
keywords: [
|
|
108
|
+
"버그",
|
|
109
|
+
"에러",
|
|
110
|
+
"오류",
|
|
111
|
+
"고쳐",
|
|
112
|
+
"수정",
|
|
113
|
+
"디버그",
|
|
114
|
+
"fix",
|
|
115
|
+
"bug",
|
|
116
|
+
"error",
|
|
117
|
+
"debug",
|
|
118
|
+
"troubleshoot",
|
|
119
|
+
"crash",
|
|
120
|
+
"broken",
|
|
121
|
+
],
|
|
122
|
+
weight: 1.0,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
category: "analyze",
|
|
126
|
+
keywords: [
|
|
127
|
+
"분석",
|
|
128
|
+
"조사",
|
|
129
|
+
"파악",
|
|
130
|
+
"analyze",
|
|
131
|
+
"investigate",
|
|
132
|
+
"examine",
|
|
133
|
+
"inspect",
|
|
134
|
+
],
|
|
135
|
+
weight: 0.9,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
category: "design",
|
|
139
|
+
keywords: [
|
|
140
|
+
"설계",
|
|
141
|
+
"아키텍처",
|
|
142
|
+
"디자인",
|
|
143
|
+
"구조",
|
|
144
|
+
"design",
|
|
145
|
+
"architect",
|
|
146
|
+
"structure",
|
|
147
|
+
],
|
|
148
|
+
weight: 0.9,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
category: "review",
|
|
152
|
+
keywords: ["리뷰", "검토", "코드리뷰", "review", "code review", "audit"],
|
|
153
|
+
weight: 1.0,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
category: "document",
|
|
157
|
+
keywords: [
|
|
158
|
+
"문서",
|
|
159
|
+
"도큐먼트",
|
|
160
|
+
"문서화",
|
|
161
|
+
"document",
|
|
162
|
+
"docs",
|
|
163
|
+
"documentation",
|
|
164
|
+
"readme",
|
|
165
|
+
],
|
|
166
|
+
weight: 0.9,
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
category: "research",
|
|
170
|
+
keywords: ["리서치", "연구", "탐색", "research", "explore", "study"],
|
|
171
|
+
weight: 0.8,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
category: "quick-fix",
|
|
175
|
+
keywords: ["빠르게", "간단히", "급한", "quick fix", "hotfix", "quick"],
|
|
176
|
+
weight: 0.85,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
category: "explain",
|
|
180
|
+
keywords: [
|
|
181
|
+
"설명",
|
|
182
|
+
"뭐야",
|
|
183
|
+
"알려",
|
|
184
|
+
"이해",
|
|
185
|
+
"explain",
|
|
186
|
+
"what is",
|
|
187
|
+
"how does",
|
|
188
|
+
"tell me",
|
|
189
|
+
"describe",
|
|
190
|
+
],
|
|
191
|
+
weight: 1.0,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
category: "test",
|
|
195
|
+
keywords: [
|
|
196
|
+
"테스트",
|
|
197
|
+
"테스팅",
|
|
198
|
+
"시험",
|
|
199
|
+
"검증",
|
|
200
|
+
"test",
|
|
201
|
+
"testing",
|
|
202
|
+
"spec",
|
|
203
|
+
"unit test",
|
|
204
|
+
],
|
|
205
|
+
weight: 1.0,
|
|
206
|
+
},
|
|
83
207
|
];
|
|
84
208
|
|
|
85
209
|
/**
|
|
@@ -89,8 +213,8 @@ const KEYWORD_PATTERNS = [
|
|
|
89
213
|
* @returns {{ category: string, confidence: number }}
|
|
90
214
|
*/
|
|
91
215
|
export function quickClassify(prompt) {
|
|
92
|
-
if (!prompt || typeof prompt !==
|
|
93
|
-
return { category:
|
|
216
|
+
if (!prompt || typeof prompt !== "string" || !prompt.trim()) {
|
|
217
|
+
return { category: "implement", confidence: 0.1 };
|
|
94
218
|
}
|
|
95
219
|
|
|
96
220
|
const lower = prompt.toLowerCase().trim();
|
|
@@ -114,11 +238,14 @@ export function quickClassify(prompt) {
|
|
|
114
238
|
}
|
|
115
239
|
|
|
116
240
|
if (!bestCategory) {
|
|
117
|
-
return { category:
|
|
241
|
+
return { category: "implement", confidence: 0.3 };
|
|
118
242
|
}
|
|
119
243
|
|
|
120
244
|
// 매칭 품질 기반 신뢰도 (0.5~0.95 범위) — matchCount 기준으로 정규화 (3개 매칭이면 최대)
|
|
121
|
-
const confidence = Math.min(
|
|
245
|
+
const confidence = Math.min(
|
|
246
|
+
0.95,
|
|
247
|
+
0.5 + (Math.min(bestMatchCount, 3) / 3) * 0.45,
|
|
248
|
+
);
|
|
122
249
|
return { category: bestCategory, confidence };
|
|
123
250
|
}
|
|
124
251
|
|
|
@@ -131,17 +258,22 @@ export function quickClassify(prompt) {
|
|
|
131
258
|
* @returns {{ category: string, confidence: number, reasoning: string, routing: { agent: string, mcp: string|null, effort: string|null } }}
|
|
132
259
|
*/
|
|
133
260
|
export function classifyIntent(prompt) {
|
|
134
|
-
const hash = _promptHash(String(prompt ??
|
|
261
|
+
const hash = _promptHash(String(prompt ?? ""));
|
|
135
262
|
|
|
136
263
|
// 캐시 확인
|
|
137
264
|
const cached = _getCached(hash);
|
|
138
265
|
if (cached) {
|
|
139
|
-
const routing =
|
|
266
|
+
const routing =
|
|
267
|
+
INTENT_CATEGORIES[cached.category] || INTENT_CATEGORIES.implement;
|
|
140
268
|
return {
|
|
141
269
|
category: cached.category,
|
|
142
270
|
confidence: cached.confidence,
|
|
143
271
|
reasoning: `cache-hit: ${cached.category} (${cached.confidence.toFixed(2)})`,
|
|
144
|
-
routing: {
|
|
272
|
+
routing: {
|
|
273
|
+
agent: routing.agent,
|
|
274
|
+
mcp: routing.mcp,
|
|
275
|
+
effort: routing.effort,
|
|
276
|
+
},
|
|
145
277
|
};
|
|
146
278
|
}
|
|
147
279
|
|
|
@@ -157,7 +289,7 @@ export function classifyIntent(prompt) {
|
|
|
157
289
|
reasoning = `keyword-match: ${category} (${confidence.toFixed(2)})`;
|
|
158
290
|
} else if (_isCodexAvailable()) {
|
|
159
291
|
// Codex triage
|
|
160
|
-
const codexResult = _tryCodexClassify(String(prompt ??
|
|
292
|
+
const codexResult = _tryCodexClassify(String(prompt ?? ""));
|
|
161
293
|
if (codexResult && codexResult.confidence > 0.8) {
|
|
162
294
|
category = codexResult.category;
|
|
163
295
|
confidence = codexResult.confidence;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const GIT_BASH_PATHS = Object.freeze([
|
|
4
|
+
"C:/Program Files/Git/bin/bash.exe",
|
|
5
|
+
"C:/Program Files/Git/usr/bin/bash.exe",
|
|
6
|
+
"C:/Program Files (x86)/Git/bin/bash.exe",
|
|
7
|
+
"C:/Program Files (x86)/Git/usr/bin/bash.exe",
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a concrete Git Bash executable on Windows.
|
|
12
|
+
*
|
|
13
|
+
* @param {object} [opts]
|
|
14
|
+
* @param {string} [opts.platform=process.platform]
|
|
15
|
+
* @param {(path: string) => boolean} [opts.exists=existsSync]
|
|
16
|
+
* @returns {string | null}
|
|
17
|
+
*/
|
|
18
|
+
export function resolveGitBashExecutable(opts = {}) {
|
|
19
|
+
const { platform = process.platform, exists = existsSync } = opts;
|
|
20
|
+
|
|
21
|
+
if (platform !== "win32") {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return GIT_BASH_PATHS.find((candidate) => exists(candidate)) || null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a Windows-safe bash executable.
|
|
30
|
+
* On Windows we prefer Git Bash over bare `bash`, which may resolve to WSL.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} [opts]
|
|
33
|
+
* @param {string} [opts.platform=process.platform]
|
|
34
|
+
* @param {(path: string) => boolean} [opts.exists=existsSync]
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function resolveBashExecutable(opts = {}) {
|
|
38
|
+
return resolveGitBashExecutable(opts) || "bash";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function shellQuote(command) {
|
|
42
|
+
return `'${String(command).replace(/'/g, `'"'"'`)}'`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Wrap .sh script command strings so Windows always runs them through Bash.
|
|
47
|
+
* This prevents shell:true child launches from treating a script path like an
|
|
48
|
+
* openable file association instead of an executable script.
|
|
49
|
+
*
|
|
50
|
+
* @param {string} command
|
|
51
|
+
* @param {object} [opts]
|
|
52
|
+
* @param {string} [opts.bashCommand]
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
export function ensureBashScriptExecution(command, opts = {}) {
|
|
56
|
+
const text = String(command || "").trim();
|
|
57
|
+
if (!text) return text;
|
|
58
|
+
|
|
59
|
+
if (!/\.sh(?:\s|$)/iu.test(text)) {
|
|
60
|
+
return text;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (
|
|
64
|
+
/^(?:\s*[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S+)\s+)*(?:bash(?:\.exe)?|sh)\b/iu.test(
|
|
65
|
+
text,
|
|
66
|
+
)
|
|
67
|
+
) {
|
|
68
|
+
return text;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const bashCommand = opts.bashCommand || resolveBashExecutable();
|
|
72
|
+
return `${bashCommand} -lc ${shellQuote(text)}`;
|
|
73
|
+
}
|
package/hub/lib/cache-guard.mjs
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
accessSync,
|
|
3
|
+
constants,
|
|
4
|
+
existsSync,
|
|
5
|
+
readdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
} from "node:fs";
|
|
2
8
|
import http from "node:http";
|
|
3
9
|
import https from "node:https";
|
|
4
10
|
import { basename, join, relative } from "node:path";
|
|
@@ -7,7 +13,10 @@ const NETWORK_TIMEOUT_MS = 3_000;
|
|
|
7
13
|
|
|
8
14
|
function toIssuePath(cacheDir, filePath) {
|
|
9
15
|
const relPath = relative(cacheDir, filePath);
|
|
10
|
-
return (relPath && relPath.length > 0 ? relPath : basename(filePath)).replace(
|
|
16
|
+
return (relPath && relPath.length > 0 ? relPath : basename(filePath)).replace(
|
|
17
|
+
/\\/g,
|
|
18
|
+
"/",
|
|
19
|
+
);
|
|
11
20
|
}
|
|
12
21
|
|
|
13
22
|
function collectCacheFiles(cacheDir) {
|
|
@@ -45,19 +54,28 @@ function probeUrl(url) {
|
|
|
45
54
|
|
|
46
55
|
try {
|
|
47
56
|
const parsed = new URL(url);
|
|
48
|
-
const transport =
|
|
57
|
+
const transport =
|
|
58
|
+
parsed.protocol === "https:"
|
|
59
|
+
? https
|
|
60
|
+
: parsed.protocol === "http:"
|
|
61
|
+
? http
|
|
62
|
+
: null;
|
|
49
63
|
if (!transport) {
|
|
50
64
|
finish(false);
|
|
51
65
|
return;
|
|
52
66
|
}
|
|
53
67
|
|
|
54
|
-
const request = transport.request(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
68
|
+
const request = transport.request(
|
|
69
|
+
parsed,
|
|
70
|
+
{
|
|
71
|
+
method: "HEAD",
|
|
72
|
+
headers: { "user-agent": "triflux-cache-guard" },
|
|
73
|
+
},
|
|
74
|
+
(response) => {
|
|
75
|
+
response.resume();
|
|
76
|
+
finish(true);
|
|
77
|
+
},
|
|
78
|
+
);
|
|
61
79
|
|
|
62
80
|
request.setTimeout(NETWORK_TIMEOUT_MS, () => {
|
|
63
81
|
request.destroy(new Error("timeout"));
|
|
@@ -88,23 +106,31 @@ export function validateRuntimeCachePaths(cacheDir) {
|
|
|
88
106
|
} catch (error) {
|
|
89
107
|
return {
|
|
90
108
|
ok: false,
|
|
91
|
-
issues: [
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
109
|
+
issues: [
|
|
110
|
+
{
|
|
111
|
+
file: ".",
|
|
112
|
+
error: error instanceof Error ? error.message : String(error),
|
|
113
|
+
},
|
|
114
|
+
],
|
|
95
115
|
};
|
|
96
116
|
}
|
|
97
117
|
}
|
|
98
118
|
|
|
99
119
|
export async function checkNetworkAvailability(urls) {
|
|
100
|
-
const targets = [
|
|
120
|
+
const targets = [
|
|
121
|
+
...new Set((Array.isArray(urls) ? urls : []).filter(Boolean)),
|
|
122
|
+
];
|
|
101
123
|
if (targets.length === 0) {
|
|
102
124
|
return { online: true, reachable: [], unreachable: [] };
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
const results = await Promise.all(targets.map((url) => probeUrl(url)));
|
|
106
|
-
const reachable = results
|
|
107
|
-
|
|
128
|
+
const reachable = results
|
|
129
|
+
.filter((result) => result.ok)
|
|
130
|
+
.map((result) => result.url);
|
|
131
|
+
const unreachable = results
|
|
132
|
+
.filter((result) => !result.ok)
|
|
133
|
+
.map((result) => result.url);
|
|
108
134
|
|
|
109
135
|
return {
|
|
110
136
|
online: unreachable.length === 0,
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// hub/lib/env-detect.mjs — 쉘/터미널/멀티플렉서 환경 감지
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { platform as osPlatform } from "node:os";
|
|
4
|
+
|
|
5
|
+
let _cached = null;
|
|
6
|
+
let _shellCache = null;
|
|
7
|
+
let _terminalCache = null;
|
|
8
|
+
let _multiplexerCache = null;
|
|
9
|
+
|
|
10
|
+
const PIPE_OPTS = { encoding: "utf8", timeout: 3000, stdio: "pipe" };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 기본 쉘 감지
|
|
14
|
+
* Windows: pwsh → powershell (full path + version)
|
|
15
|
+
* Unix: $SHELL → /bin/sh (+ version)
|
|
16
|
+
*/
|
|
17
|
+
export function detectShell() {
|
|
18
|
+
if (_shellCache) return _shellCache;
|
|
19
|
+
|
|
20
|
+
const platform = osPlatform();
|
|
21
|
+
|
|
22
|
+
if (platform === "win32") {
|
|
23
|
+
try {
|
|
24
|
+
const path = execFileSync("where", ["pwsh.exe"], PIPE_OPTS)
|
|
25
|
+
.trim()
|
|
26
|
+
.split(/\r?\n/)[0];
|
|
27
|
+
let version = null;
|
|
28
|
+
try {
|
|
29
|
+
version = execFileSync(
|
|
30
|
+
path,
|
|
31
|
+
[
|
|
32
|
+
"-NoLogo",
|
|
33
|
+
"-NoProfile",
|
|
34
|
+
"-Command",
|
|
35
|
+
"$PSVersionTable.PSVersion.ToString()",
|
|
36
|
+
],
|
|
37
|
+
PIPE_OPTS,
|
|
38
|
+
).trim();
|
|
39
|
+
} catch {}
|
|
40
|
+
_shellCache = { name: "pwsh", path, version };
|
|
41
|
+
} catch {
|
|
42
|
+
try {
|
|
43
|
+
const path = execFileSync("where", ["powershell.exe"], PIPE_OPTS)
|
|
44
|
+
.trim()
|
|
45
|
+
.split(/\r?\n/)[0];
|
|
46
|
+
_shellCache = { name: "powershell", path, version: null };
|
|
47
|
+
} catch {
|
|
48
|
+
_shellCache = {
|
|
49
|
+
name: "powershell",
|
|
50
|
+
path: "",
|
|
51
|
+
version: null,
|
|
52
|
+
installHint: "pwsh: winget install Microsoft.PowerShell",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return _shellCache;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const shellPath = process.env.SHELL || "/bin/sh";
|
|
60
|
+
const name = shellPath.split("/").pop() || "sh";
|
|
61
|
+
let version = null;
|
|
62
|
+
try {
|
|
63
|
+
version = execFileSync(shellPath, ["--version"], PIPE_OPTS).trim();
|
|
64
|
+
} catch {}
|
|
65
|
+
_shellCache = { name, path: shellPath, version };
|
|
66
|
+
return _shellCache;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 터미널 에뮬레이터 감지
|
|
71
|
+
*/
|
|
72
|
+
export function detectTerminal() {
|
|
73
|
+
if (_terminalCache) return _terminalCache;
|
|
74
|
+
|
|
75
|
+
const platform = osPlatform();
|
|
76
|
+
if (platform === "win32") {
|
|
77
|
+
try {
|
|
78
|
+
execFileSync("where", ["wt.exe"], PIPE_OPTS);
|
|
79
|
+
_terminalCache = { name: "windows-terminal", hasWt: true };
|
|
80
|
+
} catch {
|
|
81
|
+
_terminalCache = {
|
|
82
|
+
name: "unknown",
|
|
83
|
+
hasWt: false,
|
|
84
|
+
installHint: "wt: winget install Microsoft.WindowsTerminal",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return _terminalCache;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (process.env.TERM_PROGRAM === "WarpTerminal") {
|
|
91
|
+
_terminalCache = { name: "warp", hasWt: false };
|
|
92
|
+
} else if (process.env.TERM_PROGRAM === "Alacritty") {
|
|
93
|
+
_terminalCache = { name: "alacritty", hasWt: false };
|
|
94
|
+
} else if (process.env.KITTY_WINDOW_ID) {
|
|
95
|
+
_terminalCache = { name: "kitty", hasWt: false };
|
|
96
|
+
} else if (process.env.TERM_PROGRAM === "iTerm.app") {
|
|
97
|
+
_terminalCache = { name: "iterm2", hasWt: false };
|
|
98
|
+
} else if (process.env.TERM_PROGRAM === "Apple_Terminal") {
|
|
99
|
+
_terminalCache = { name: "terminal-app", hasWt: false };
|
|
100
|
+
} else {
|
|
101
|
+
_terminalCache = { name: "unknown", hasWt: false };
|
|
102
|
+
}
|
|
103
|
+
return _terminalCache;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 멀티플렉서 감지 (tmux)
|
|
108
|
+
*/
|
|
109
|
+
export function detectMultiplexer() {
|
|
110
|
+
if (_multiplexerCache) return _multiplexerCache;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const cmd = osPlatform() === "win32" ? "where" : "which";
|
|
114
|
+
const path = execFileSync(cmd, ["tmux"], PIPE_OPTS).trim();
|
|
115
|
+
_multiplexerCache = { name: "tmux", path };
|
|
116
|
+
} catch {
|
|
117
|
+
const hint =
|
|
118
|
+
osPlatform() === "win32"
|
|
119
|
+
? "tmux: install tmux in WSL or MSYS2"
|
|
120
|
+
: undefined;
|
|
121
|
+
_multiplexerCache = {
|
|
122
|
+
name: "none",
|
|
123
|
+
path: null,
|
|
124
|
+
...(hint ? { installHint: hint } : {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return _multiplexerCache;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 통합 환경 정보 조회 (레이지 싱글톤 캐시)
|
|
132
|
+
* @returns {{ shell: object, terminal: object, multiplexer: object, platform: string }}
|
|
133
|
+
*/
|
|
134
|
+
export function getEnvironment() {
|
|
135
|
+
if (_cached) return _cached;
|
|
136
|
+
|
|
137
|
+
_cached = Object.freeze({
|
|
138
|
+
shell: detectShell(),
|
|
139
|
+
terminal: detectTerminal(),
|
|
140
|
+
multiplexer: detectMultiplexer(),
|
|
141
|
+
platform: osPlatform(),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return _cached;
|
|
145
|
+
}
|