@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.
Files changed (143) hide show
  1. package/hooks/agent-route-guard.mjs +14 -10
  2. package/hooks/cross-review-tracker.mjs +21 -8
  3. package/hooks/error-context.mjs +47 -7
  4. package/hooks/hook-adaptive-collector.mjs +18 -16
  5. package/hooks/hook-manager.mjs +93 -32
  6. package/hooks/hook-orchestrator.mjs +279 -33
  7. package/hooks/hook-registry.json +104 -7
  8. package/hooks/hooks.json +49 -1
  9. package/hooks/keyword-rules.json +183 -23
  10. package/hooks/lib/resolve-root.mjs +1 -1
  11. package/hooks/mcp-config-watcher.mjs +6 -2
  12. package/hooks/permission-safe-allow.mjs +233 -0
  13. package/hooks/pipeline-stop.mjs +369 -22
  14. package/hooks/post-tool-tips.mjs +306 -0
  15. package/hooks/pre-compact-snapshot.mjs +181 -0
  16. package/hooks/safety-guard.mjs +308 -18
  17. package/hooks/session-end-cleanup.mjs +345 -0
  18. package/hooks/session-start-fast.mjs +214 -0
  19. package/hooks/subagent-tracker.mjs +267 -0
  20. package/hooks/subagent-verifier.mjs +9 -4
  21. package/hub/account-broker.mjs +1036 -66
  22. package/hub/adaptive-diagnostic.mjs +75 -48
  23. package/hub/adaptive-inject.mjs +96 -58
  24. package/hub/adaptive-memory.mjs +156 -42
  25. package/hub/adaptive.mjs +60 -31
  26. package/hub/assign-callbacks.mjs +163 -126
  27. package/hub/bridge.mjs +832 -291
  28. package/hub/cli-adapter-base.mjs +321 -97
  29. package/hub/codex-adapter.mjs +179 -93
  30. package/hub/codex-compat.mjs +4 -3
  31. package/hub/codex-preflight.mjs +113 -37
  32. package/hub/delegator/contracts.mjs +19 -23
  33. package/hub/delegator/index.mjs +14 -14
  34. package/hub/delegator/service.mjs +88 -64
  35. package/hub/delegator/tool-definitions.mjs +35 -35
  36. package/hub/dynamic-routing-engine.mjs +511 -0
  37. package/hub/fullcycle.mjs +34 -17
  38. package/hub/gemini-adapter.mjs +77 -95
  39. package/hub/hitl.mjs +202 -143
  40. package/hub/intent.mjs +171 -39
  41. package/hub/lib/bash-path.mjs +73 -0
  42. package/hub/lib/cache-guard.mjs +43 -17
  43. package/hub/lib/env-detect.mjs +145 -0
  44. package/hub/lib/hosts-compat.mjs +319 -0
  45. package/hub/lib/mcp-response-cache.mjs +239 -0
  46. package/hub/lib/memory-store.mjs +303 -107
  47. package/hub/lib/path-utils.mjs +165 -0
  48. package/hub/lib/phase-manager.mjs +493 -0
  49. package/hub/lib/process-utils.mjs +797 -104
  50. package/hub/lib/prompt-tmp.mjs +33 -0
  51. package/hub/lib/risk-tier.mjs +53 -0
  52. package/hub/lib/spawn-trace.mjs +561 -0
  53. package/hub/lib/ssh-command.mjs +44 -24
  54. package/hub/lib/ssh-retry.mjs +5 -1
  55. package/hub/lib/staged-file-detect.mjs +43 -0
  56. package/hub/lib/state-snapshot.mjs +293 -0
  57. package/hub/lib/tfx-route-args.mjs +309 -0
  58. package/hub/lib/timeline-adapter.mjs +190 -0
  59. package/hub/lib/trace-recorder.mjs +153 -0
  60. package/hub/lib/uuidv7.mjs +4 -3
  61. package/hub/memory-doctor.mjs +266 -106
  62. package/hub/middleware/quota-middleware.mjs +70 -0
  63. package/hub/middleware/request-logger.mjs +61 -34
  64. package/hub/paths.mjs +9 -9
  65. package/hub/pipeline/gates/confidence.mjs +34 -15
  66. package/hub/pipeline/gates/consensus.mjs +27 -15
  67. package/hub/pipeline/gates/index.mjs +7 -3
  68. package/hub/pipeline/gates/selfcheck.mjs +57 -19
  69. package/hub/pipeline/index.mjs +77 -42
  70. package/hub/pipeline/state.mjs +10 -10
  71. package/hub/pipeline/transitions.mjs +40 -23
  72. package/hub/platform.mjs +65 -48
  73. package/hub/quality/deslop.mjs +70 -49
  74. package/hub/reflexion.mjs +116 -58
  75. package/hub/research.mjs +34 -25
  76. package/hub/router.mjs +941 -791
  77. package/hub/routing/complexity.mjs +132 -29
  78. package/hub/routing/index.mjs +17 -12
  79. package/hub/routing/q-learning.mjs +76 -28
  80. package/hub/routing-snapshot.mjs +261 -0
  81. package/hub/session-fingerprint.mjs +129 -63
  82. package/hub/state.mjs +86 -44
  83. package/hub/team/retry-state-machine.mjs +267 -0
  84. package/hub/token-mode.mjs +76 -72
  85. package/hub/workers/worker-utils.mjs +22 -16
  86. package/hud/colors.mjs +27 -9
  87. package/hud/constants.mjs +184 -27
  88. package/hud/context-monitor.mjs +133 -44
  89. package/hud/hud-qos-status.mjs +153 -50
  90. package/hud/mission-board.mjs +56 -0
  91. package/hud/providers/claude.mjs +398 -114
  92. package/hud/providers/codex.mjs +96 -21
  93. package/hud/providers/gemini.mjs +184 -51
  94. package/hud/renderers.mjs +413 -123
  95. package/hud/terminal.mjs +101 -31
  96. package/hud/utils.mjs +81 -39
  97. package/mesh/index.mjs +69 -0
  98. package/mesh/mesh-budget.mjs +137 -0
  99. package/mesh/mesh-heartbeat.mjs +100 -0
  100. package/mesh/mesh-protocol.mjs +96 -0
  101. package/mesh/mesh-queue.mjs +163 -0
  102. package/mesh/mesh-registry.mjs +78 -0
  103. package/mesh/mesh-router.mjs +77 -0
  104. package/package.json +5 -1
  105. package/scripts/lib/agent-json.mjs +27 -0
  106. package/scripts/lib/async.mjs +174 -0
  107. package/scripts/lib/claudemd-scanner.mjs +6 -1
  108. package/scripts/lib/cli-agy.mjs +62 -0
  109. package/scripts/lib/cli-claude.mjs +78 -0
  110. package/scripts/lib/cli-codex.mjs +199 -0
  111. package/scripts/lib/cli-gemini.mjs +67 -0
  112. package/scripts/lib/codex-recovery.sh +50 -0
  113. package/scripts/lib/context.mjs +3 -3
  114. package/scripts/lib/cross-review-utils.mjs +13 -5
  115. package/scripts/lib/doctor-env-checks.mjs +121 -0
  116. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  117. package/scripts/lib/env-probe.mjs +58 -30
  118. package/scripts/lib/env.mjs +14 -0
  119. package/scripts/lib/gemini-profiles.mjs +44 -10
  120. package/scripts/lib/handoff.mjs +33 -17
  121. package/scripts/lib/hook-utils.mjs +8 -6
  122. package/scripts/lib/hub.mjs +48 -0
  123. package/scripts/lib/keyword-rules.mjs +48 -19
  124. package/scripts/lib/logger.mjs +24 -24
  125. package/scripts/lib/mcp-filter.mjs +393 -249
  126. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  127. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  128. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  129. package/scripts/lib/mcp-guard-engine.mjs +1282 -116
  130. package/scripts/lib/mcp-health.mjs +773 -0
  131. package/scripts/lib/mcp-manifest.mjs +24 -14
  132. package/scripts/lib/mcp-server-catalog.mjs +355 -118
  133. package/scripts/lib/pid.mjs +63 -0
  134. package/scripts/lib/process-utils.mjs +26 -0
  135. package/scripts/lib/psmux-info.mjs +72 -12
  136. package/scripts/lib/quota.mjs +47 -0
  137. package/scripts/lib/remote-spawn-transfer.mjs +44 -14
  138. package/scripts/lib/skill-state.mjs +220 -0
  139. package/scripts/lib/skill-template.mjs +36 -11
  140. package/scripts/lib/team.mjs +75 -0
  141. package/scripts/lib/timeout.mjs +66 -0
  142. package/scripts/lib/tmp.mjs +26 -0
  143. 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 'node:child_process';
5
- import crypto from 'node:crypto';
6
- import { whichCommand } from './platform.mjs';
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('codex'));
18
+ _codexAvailable = Boolean(whichCommand("codex"));
18
19
  return _codexAvailable;
19
20
  }
20
21
 
21
22
  function _promptHash(prompt) {
22
- return crypto.createHash('md5').update(prompt).digest('hex');
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
- 'codex',
39
- ['exec', `Classify intent: ${prompt}. Reply JSON: {intent, confidence}`],
40
- { timeout: 8000, encoding: 'utf8' }
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 === 'string' ? parsed.intent : null;
47
- const confidence = typeof parsed.confidence === 'number' ? parsed.confidence : null;
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
- /** triflux 특화 의도 카테고리 (10개) */
59
+ /**
60
+ * @experimental
61
+ * triflux 특화 의도 카테고리 (10개)
62
+ */
58
63
  export const INTENT_CATEGORIES = {
59
- implement: { agent: 'executor', mcp: 'implement', effort: 'codex53_high' },
60
- debug: { agent: 'debugger', mcp: 'implement', effort: 'codex53_high' },
61
- analyze: { agent: 'analyst', mcp: 'analyze', effort: 'gpt54_xhigh' },
62
- design: { agent: 'architect', mcp: 'analyze', effort: 'gpt54_xhigh' },
63
- review: { agent: 'code-reviewer', mcp: 'review', effort: 'codex53_high' },
64
- document: { agent: 'writer', mcp: 'docs', effort: 'pro' },
65
- research: { agent: 'scientist', mcp: 'analyze', effort: 'codex53_high' },
66
- 'quick-fix':{ agent: 'build-fixer', mcp: 'implement', effort: 'codex53_low' },
67
- explain: { agent: 'writer', mcp: 'docs', effort: 'flash' },
68
- test: { agent: 'test-engineer', mcp: null, effort: null },
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
- { category: 'implement', keywords: ['구현', '만들', '추가', '생성', '작성', '빌드', 'implement', 'create', 'add', 'build', 'make', 'develop'], weight: 1.0 },
74
- { category: 'debug', keywords: ['버그', '에러', '오류', '고쳐', '수정', '디버그', 'fix', 'bug', 'error', 'debug', 'troubleshoot', 'crash', 'broken'], weight: 1.0 },
75
- { category: 'analyze', keywords: ['분석', '조사', '파악', 'analyze', 'investigate', 'examine', 'inspect'], weight: 0.9 },
76
- { category: 'design', keywords: ['설계', '아키텍처', '디자인', '구조', 'design', 'architect', 'structure'], weight: 0.9 },
77
- { category: 'review', keywords: ['리뷰', '검토', '코드리뷰', 'review', 'code review', 'audit'], weight: 1.0 },
78
- { category: 'document', keywords: ['문서', '도큐먼트', '문서화', 'document', 'docs', 'documentation', 'readme'], weight: 0.9 },
79
- { category: 'research', keywords: ['리서치', '연구', '탐색', 'research', 'explore', 'study'], weight: 0.8 },
80
- { category: 'quick-fix', keywords: ['빠르게', '간단히', '급한', 'quick fix', 'hotfix', 'quick'], weight: 0.85 },
81
- { category: 'explain', keywords: ['설명', '뭐야', '알려', '이해', 'explain', 'what is', 'how does', 'tell me', 'describe'], weight: 1.0 },
82
- { category: 'test', keywords: ['테스트', '테스팅', '시험', '검증', 'test', 'testing', 'spec', 'unit test'], weight: 1.0 },
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 !== 'string' || !prompt.trim()) {
93
- return { category: 'implement', confidence: 0.1 };
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: 'implement', confidence: 0.3 };
241
+ return { category: "implement", confidence: 0.3 };
118
242
  }
119
243
 
120
244
  // 매칭 품질 기반 신뢰도 (0.5~0.95 범위) — matchCount 기준으로 정규화 (3개 매칭이면 최대)
121
- const confidence = Math.min(0.95, 0.5 + (Math.min(bestMatchCount, 3) / 3) * 0.45);
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 = INTENT_CATEGORIES[cached.category] || INTENT_CATEGORIES.implement;
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: { agent: routing.agent, mcp: routing.mcp, effort: routing.effort },
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
+ }
@@ -1,4 +1,10 @@
1
- import { accessSync, constants, existsSync, readFileSync, readdirSync } from "node:fs";
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(/\\/g, "/");
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 = parsed.protocol === "https:" ? https : parsed.protocol === "http:" ? http : null;
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(parsed, {
55
- method: "HEAD",
56
- headers: { "user-agent": "triflux-cache-guard" },
57
- }, (response) => {
58
- response.resume();
59
- finish(true);
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
- file: ".",
93
- error: error instanceof Error ? error.message : String(error),
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 = [...new Set((Array.isArray(urls) ? urls : []).filter(Boolean))];
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.filter((result) => result.ok).map((result) => result.url);
107
- const unreachable = results.filter((result) => !result.ok).map((result) => result.url);
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
+ }