@triflux/core 10.1.0 → 10.26.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/agent-route-guard.mjs +14 -10
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +20 -8
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +267 -35
- package/hooks/hook-registry.json +343 -256
- package/hooks/hooks.json +49 -1
- package/hooks/keyword-rules.json +191 -31
- 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 +262 -34
- 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 +1051 -290
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -4
- package/hub/codex-preflight.mjs +113 -37
- package/hub/delegator/contracts.mjs +22 -21
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/schema/delegator-tools.schema.json +1 -1
- package/hub/delegator/service.mjs +93 -64
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/dynamic-routing-engine.mjs +511 -0
- package/hub/fullcycle.mjs +34 -18
- package/hub/gemini-adapter.mjs +82 -108
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +167 -38
- 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 +285 -111
- 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 +87 -13
- package/hub/research.mjs +33 -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 +78 -30
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/claude-daemon-control.mjs +1130 -0
- package/hub/team/retry-state-machine.mjs +336 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +23 -17
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +190 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +157 -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 +210 -52
- package/hud/renderers.mjs +440 -127
- 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 +37 -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 +49 -20
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +391 -247
- 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 +1281 -115
- 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 +117 -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 +32 -7
- 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;
|
|
@@ -59,30 +61,149 @@ function _tryCodexClassify(prompt) {
|
|
|
59
61
|
* triflux 특화 의도 카테고리 (10개)
|
|
60
62
|
*/
|
|
61
63
|
export const INTENT_CATEGORIES = {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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 },
|
|
72
83
|
};
|
|
73
84
|
|
|
74
85
|
/** @internal 키워드 → 카테고리 매핑 패턴 */
|
|
75
86
|
const KEYWORD_PATTERNS = [
|
|
76
|
-
{
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
+
},
|
|
86
207
|
];
|
|
87
208
|
|
|
88
209
|
/**
|
|
@@ -92,8 +213,8 @@ const KEYWORD_PATTERNS = [
|
|
|
92
213
|
* @returns {{ category: string, confidence: number }}
|
|
93
214
|
*/
|
|
94
215
|
export function quickClassify(prompt) {
|
|
95
|
-
if (!prompt || typeof prompt !==
|
|
96
|
-
return { category:
|
|
216
|
+
if (!prompt || typeof prompt !== "string" || !prompt.trim()) {
|
|
217
|
+
return { category: "implement", confidence: 0.1 };
|
|
97
218
|
}
|
|
98
219
|
|
|
99
220
|
const lower = prompt.toLowerCase().trim();
|
|
@@ -117,11 +238,14 @@ export function quickClassify(prompt) {
|
|
|
117
238
|
}
|
|
118
239
|
|
|
119
240
|
if (!bestCategory) {
|
|
120
|
-
return { category:
|
|
241
|
+
return { category: "implement", confidence: 0.3 };
|
|
121
242
|
}
|
|
122
243
|
|
|
123
244
|
// 매칭 품질 기반 신뢰도 (0.5~0.95 범위) — matchCount 기준으로 정규화 (3개 매칭이면 최대)
|
|
124
|
-
const confidence = Math.min(
|
|
245
|
+
const confidence = Math.min(
|
|
246
|
+
0.95,
|
|
247
|
+
0.5 + (Math.min(bestMatchCount, 3) / 3) * 0.45,
|
|
248
|
+
);
|
|
125
249
|
return { category: bestCategory, confidence };
|
|
126
250
|
}
|
|
127
251
|
|
|
@@ -134,17 +258,22 @@ export function quickClassify(prompt) {
|
|
|
134
258
|
* @returns {{ category: string, confidence: number, reasoning: string, routing: { agent: string, mcp: string|null, effort: string|null } }}
|
|
135
259
|
*/
|
|
136
260
|
export function classifyIntent(prompt) {
|
|
137
|
-
const hash = _promptHash(String(prompt ??
|
|
261
|
+
const hash = _promptHash(String(prompt ?? ""));
|
|
138
262
|
|
|
139
263
|
// 캐시 확인
|
|
140
264
|
const cached = _getCached(hash);
|
|
141
265
|
if (cached) {
|
|
142
|
-
const routing =
|
|
266
|
+
const routing =
|
|
267
|
+
INTENT_CATEGORIES[cached.category] || INTENT_CATEGORIES.implement;
|
|
143
268
|
return {
|
|
144
269
|
category: cached.category,
|
|
145
270
|
confidence: cached.confidence,
|
|
146
271
|
reasoning: `cache-hit: ${cached.category} (${cached.confidence.toFixed(2)})`,
|
|
147
|
-
routing: {
|
|
272
|
+
routing: {
|
|
273
|
+
agent: routing.agent,
|
|
274
|
+
mcp: routing.mcp,
|
|
275
|
+
effort: routing.effort,
|
|
276
|
+
},
|
|
148
277
|
};
|
|
149
278
|
}
|
|
150
279
|
|
|
@@ -160,7 +289,7 @@ export function classifyIntent(prompt) {
|
|
|
160
289
|
reasoning = `keyword-match: ${category} (${confidence.toFixed(2)})`;
|
|
161
290
|
} else if (_isCodexAvailable()) {
|
|
162
291
|
// Codex triage
|
|
163
|
-
const codexResult = _tryCodexClassify(String(prompt ??
|
|
292
|
+
const codexResult = _tryCodexClassify(String(prompt ?? ""));
|
|
164
293
|
if (codexResult && codexResult.confidence > 0.8) {
|
|
165
294
|
category = codexResult.category;
|
|
166
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
|
+
}
|