@vibecodetown/mcp-server 2.1.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.
Files changed (172) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +269 -0
  3. package/build/auth/gate.js +225 -0
  4. package/build/auth/index.js +55 -0
  5. package/build/auth/public_key.js +27 -0
  6. package/build/auth/token_cache.js +122 -0
  7. package/build/auth/token_verifier.js +103 -0
  8. package/build/bootstrap/doctor.js +115 -0
  9. package/build/bootstrap/installer.js +673 -0
  10. package/build/bootstrap/lock.js +37 -0
  11. package/build/bootstrap/platform.js +26 -0
  12. package/build/bootstrap/registry.js +37 -0
  13. package/build/cache/index.js +147 -0
  14. package/build/cli.js +101 -0
  15. package/build/contracts.js +22 -0
  16. package/build/control_plane/gate.js +161 -0
  17. package/build/control_plane/index.js +6 -0
  18. package/build/dx/activity.js +139 -0
  19. package/build/engine.js +106 -0
  20. package/build/errors.js +171 -0
  21. package/build/generated/activate_input.js +2 -0
  22. package/build/generated/activate_output.js +57 -0
  23. package/build/generated/advisory_review_input.js +2 -0
  24. package/build/generated/advisory_review_output.js +35 -0
  25. package/build/generated/auth_token_file.js +2 -0
  26. package/build/generated/briefing_input.js +2 -0
  27. package/build/generated/briefing_output.js +2 -0
  28. package/build/generated/clinic_bridge_file.js +13 -0
  29. package/build/generated/contracts_bundle_info.js +5 -0
  30. package/build/generated/create_work_order_input.js +2 -0
  31. package/build/generated/create_work_order_output.js +2 -0
  32. package/build/generated/current_work_order_file.js +2 -0
  33. package/build/generated/doctor_input.js +2 -0
  34. package/build/generated/doctor_output.js +24 -0
  35. package/build/generated/execution_result.js +2 -0
  36. package/build/generated/execution_task.js +2 -0
  37. package/build/generated/export_output_input.js +2 -0
  38. package/build/generated/export_output_output.js +2 -0
  39. package/build/generated/finalize_work_input.js +2 -0
  40. package/build/generated/finalize_work_output.js +2 -0
  41. package/build/generated/gate_input.js +2 -0
  42. package/build/generated/gate_output.js +2 -0
  43. package/build/generated/gate_result_v1.js +2 -0
  44. package/build/generated/get_decision_input.js +2 -0
  45. package/build/generated/get_decision_output.js +13 -0
  46. package/build/generated/handoff_to_clinic.js +2 -0
  47. package/build/generated/index.js +75 -0
  48. package/build/generated/inspect_code_input.js +2 -0
  49. package/build/generated/inspect_code_output.js +13 -0
  50. package/build/generated/memory_retrieve_output.js +2 -0
  51. package/build/generated/memory_state_file.js +2 -0
  52. package/build/generated/memory_status_input.js +2 -0
  53. package/build/generated/memory_status_output.js +13 -0
  54. package/build/generated/memory_sync_input.js +2 -0
  55. package/build/generated/memory_sync_output.js +13 -0
  56. package/build/generated/plugin_result.js +2 -0
  57. package/build/generated/react_perf_check_patterns_input.js +2 -0
  58. package/build/generated/react_perf_check_patterns_output.js +2 -0
  59. package/build/generated/react_perf_generate_report_input.js +2 -0
  60. package/build/generated/react_perf_generate_report_output.js +2 -0
  61. package/build/generated/repair_plan_input.js +2 -0
  62. package/build/generated/repair_plan_output.js +2 -0
  63. package/build/generated/run_app_input.js +2 -0
  64. package/build/generated/run_app_output.js +2 -0
  65. package/build/generated/run_state_file.js +13 -0
  66. package/build/generated/scaffold_input.js +2 -0
  67. package/build/generated/scaffold_output.js +2 -0
  68. package/build/generated/search_oss_input.js +2 -0
  69. package/build/generated/search_oss_output.js +2 -0
  70. package/build/generated/selection_validation_result.js +2 -0
  71. package/build/generated/signal_agent_input.js +2 -0
  72. package/build/generated/spec_high_ask_queue_items_file.js +2 -0
  73. package/build/generated/spec_high_clinic_bridge_output.js +2 -0
  74. package/build/generated/spec_high_decision_draft_output.js +2 -0
  75. package/build/generated/spec_high_validate_output.js +2 -0
  76. package/build/generated/status_input.js +2 -0
  77. package/build/generated/status_output.js +2 -0
  78. package/build/generated/submit_decision_input.js +2 -0
  79. package/build/generated/submit_decision_output.js +2 -0
  80. package/build/generated/tool_error_output.js +2 -0
  81. package/build/generated/undo_last_task_input.js +2 -0
  82. package/build/generated/undo_last_task_output.js +2 -0
  83. package/build/generated/update_input.js +2 -0
  84. package/build/generated/update_output.js +2 -0
  85. package/build/generated/vibe_pm_inspection_result.js +2 -0
  86. package/build/generated/vibe_pm_report_markdown.js +2 -0
  87. package/build/generated/vibe_pm_verdict.js +2 -0
  88. package/build/generated/vibe_repo_config.js +2 -0
  89. package/build/generated/vibecoding_helper_answer_output.js +2 -0
  90. package/build/generated/vibecoding_helper_one_loop_selection_output.js +2 -0
  91. package/build/generated/vibecoding_helper_show_ask_queue_output.js +2 -0
  92. package/build/generated/work_order_v1.js +2 -0
  93. package/build/generated/zoekt_evidence_input.js +2 -0
  94. package/build/generated/zoekt_evidence_output.js +2 -0
  95. package/build/index.js +111 -0
  96. package/build/legacy_alias.js +65 -0
  97. package/build/local-mode/bash.js +61 -0
  98. package/build/local-mode/config.js +171 -0
  99. package/build/local-mode/git.js +33 -0
  100. package/build/local-mode/init.js +110 -0
  101. package/build/local-mode/paths.js +24 -0
  102. package/build/local-mode/templates.js +856 -0
  103. package/build/local-mode/work-order.js +41 -0
  104. package/build/resources/index.js +246 -0
  105. package/build/security/input-validator.js +119 -0
  106. package/build/security/path-policy.js +289 -0
  107. package/build/security/sandbox.js +228 -0
  108. package/build/tools/react_perf/check_patterns.js +172 -0
  109. package/build/tools/react_perf/generate_report.js +337 -0
  110. package/build/tools/react_perf/index.js +119 -0
  111. package/build/tools/react_perf/rules/advanced.js +325 -0
  112. package/build/tools/react_perf/rules/async.js +104 -0
  113. package/build/tools/react_perf/rules/bundle.js +101 -0
  114. package/build/tools/react_perf/rules/client.js +186 -0
  115. package/build/tools/react_perf/rules/index.js +74 -0
  116. package/build/tools/react_perf/rules/js.js +148 -0
  117. package/build/tools/react_perf/rules/rendering.js +166 -0
  118. package/build/tools/react_perf/rules/rerender.js +161 -0
  119. package/build/tools/react_perf/rules/server.js +141 -0
  120. package/build/tools/react_perf/types.js +127 -0
  121. package/build/tools/vibe_pm/activate.js +102 -0
  122. package/build/tools/vibe_pm/advisory_review.js +77 -0
  123. package/build/tools/vibe_pm/briefing.js +178 -0
  124. package/build/tools/vibe_pm/context.js +439 -0
  125. package/build/tools/vibe_pm/create_work_order.js +271 -0
  126. package/build/tools/vibe_pm/doc_status_gate.js +370 -0
  127. package/build/tools/vibe_pm/doctor.js +262 -0
  128. package/build/tools/vibe_pm/entity_gate/preflight.js +78 -0
  129. package/build/tools/vibe_pm/export_output.js +135 -0
  130. package/build/tools/vibe_pm/finalize_work.js +393 -0
  131. package/build/tools/vibe_pm/gate.js +33 -0
  132. package/build/tools/vibe_pm/get_decision.js +281 -0
  133. package/build/tools/vibe_pm/index.js +593 -0
  134. package/build/tools/vibe_pm/inspect_code.js +828 -0
  135. package/build/tools/vibe_pm/intent/generator.js +294 -0
  136. package/build/tools/vibe_pm/intent/index.js +5 -0
  137. package/build/tools/vibe_pm/intent/prompt_density.js +227 -0
  138. package/build/tools/vibe_pm/intent/types.js +70 -0
  139. package/build/tools/vibe_pm/intent/verifier.js +237 -0
  140. package/build/tools/vibe_pm/kce/doc_usage.js +51 -0
  141. package/build/tools/vibe_pm/kce/on_finalize.js +11 -0
  142. package/build/tools/vibe_pm/kce/preflight.js +232 -0
  143. package/build/tools/vibe_pm/local_memory.js +26 -0
  144. package/build/tools/vibe_pm/memory_status.js +82 -0
  145. package/build/tools/vibe_pm/memory_sync.js +134 -0
  146. package/build/tools/vibe_pm/modules/decision_snapshot.js +29 -0
  147. package/build/tools/vibe_pm/modules/ensure.js +100 -0
  148. package/build/tools/vibe_pm/modules/fingerprint.js +30 -0
  149. package/build/tools/vibe_pm/modules/fix_dependencies.js +394 -0
  150. package/build/tools/vibe_pm/modules/planning_v1.js +110 -0
  151. package/build/tools/vibe_pm/modules/repo_context.js +56 -0
  152. package/build/tools/vibe_pm/modules/research_v1.js +114 -0
  153. package/build/tools/vibe_pm/modules/skills_v1.js +100 -0
  154. package/build/tools/vibe_pm/pm_language.js +222 -0
  155. package/build/tools/vibe_pm/repair_plan.js +199 -0
  156. package/build/tools/vibe_pm/run_app.js +597 -0
  157. package/build/tools/vibe_pm/run_app_podman.js +64 -0
  158. package/build/tools/vibe_pm/scaffold.js +550 -0
  159. package/build/tools/vibe_pm/search_oss.js +124 -0
  160. package/build/tools/vibe_pm/status.js +153 -0
  161. package/build/tools/vibe_pm/submit_decision.js +87 -0
  162. package/build/tools/vibe_pm/system_design/issue_mapping.js +47 -0
  163. package/build/tools/vibe_pm/system_design/rulebook.js +112 -0
  164. package/build/tools/vibe_pm/system_design/semgrep.js +132 -0
  165. package/build/tools/vibe_pm/types.js +229 -0
  166. package/build/tools/vibe_pm/undo_last_task.js +163 -0
  167. package/build/tools/vibe_pm/update.js +146 -0
  168. package/build/tools/vibe_pm/zoekt_evidence.js +96 -0
  169. package/build/tools.js +269 -0
  170. package/build/version-check.js +239 -0
  171. package/build/vibe-cli.js +631 -0
  172. package/package.json +76 -0
@@ -0,0 +1,673 @@
1
+ // adapters/mcp-ts/src/bootstrap/installer.ts
2
+ // Download, verify, extract, and cache engine binaries
3
+ // With self-healing: version check, retry logic, cache validation
4
+ import fs from "node:fs";
5
+ import os from "node:os";
6
+ import path from "node:path";
7
+ import crypto from "node:crypto";
8
+ import { execFile } from "node:child_process";
9
+ import { promisify } from "node:util";
10
+ import { ENGINE_SPECS } from "./registry.js";
11
+ import { detectPlatform, exeName } from "./platform.js";
12
+ import { withLock } from "./lock.js";
13
+ const execFileAsync = promisify(execFile);
14
+ // ============================================================
15
+ // Configuration
16
+ // ============================================================
17
+ const MAX_RETRIES = 3;
18
+ const RETRY_DELAY_MS = 1000;
19
+ const DOWNLOAD_TIMEOUT_MS = 60_000;
20
+ function isTruthyEnv(value) {
21
+ if (!value)
22
+ return false;
23
+ const normalized = value.trim().toLowerCase();
24
+ return ["1", "true", "yes", "on"].includes(normalized);
25
+ }
26
+ function isOfflineMode() {
27
+ return isTruthyEnv(process.env.VIBECODE_OFFLINE);
28
+ }
29
+ function isDebugMode() {
30
+ return isTruthyEnv(process.env.VIBECODE_DEBUG);
31
+ }
32
+ // ============================================================
33
+ // Cache Management
34
+ // ============================================================
35
+ /**
36
+ * Get platform-appropriate cache root directory
37
+ */
38
+ export function cacheRoot() {
39
+ const override = (process.env.VIBECODE_CACHE ?? "").trim();
40
+ if (override) {
41
+ return path.resolve(override);
42
+ }
43
+ const home = os.homedir();
44
+ if (process.platform === "darwin") {
45
+ return path.join(home, "Library", "Caches", "vibecode");
46
+ }
47
+ if (process.platform === "win32") {
48
+ return path.join(process.env.LOCALAPPDATA || path.join(home, "AppData", "Local"), "vibecode");
49
+ }
50
+ // linux
51
+ return path.join(process.env.XDG_CACHE_HOME || path.join(home, ".cache"), "vibecode");
52
+ }
53
+ function engineDir(name, version, platform) {
54
+ return path.join(cacheRoot(), "engines", name, version, platform);
55
+ }
56
+ function versionFile(name) {
57
+ return path.join(cacheRoot(), "engines", name, ".current_version");
58
+ }
59
+ function parseSearchPaths(raw) {
60
+ if (!raw)
61
+ return [];
62
+ return raw
63
+ .split(path.delimiter)
64
+ .map((p) => p.trim())
65
+ .filter(Boolean);
66
+ }
67
+ async function resolveExternalBinary(name, platform) {
68
+ const spec = ENGINE_SPECS[name];
69
+ const roots = parseSearchPaths(process.env.VIBECODE_BIN_PATH);
70
+ if (roots.length === 0)
71
+ return null;
72
+ const exe = exeName(spec.assetPrefix);
73
+ const candidates = [];
74
+ for (const rawRoot of roots) {
75
+ const root = path.resolve(rawRoot);
76
+ candidates.push(root);
77
+ candidates.push(path.join(root, exe));
78
+ candidates.push(path.join(root, spec.assetPrefix, exe));
79
+ }
80
+ // De-dupe while preserving order
81
+ const seen = new Set();
82
+ for (const c of candidates) {
83
+ if (seen.has(c))
84
+ continue;
85
+ seen.add(c);
86
+ try {
87
+ const stat = await fs.promises.stat(c);
88
+ if (!stat.isFile())
89
+ continue;
90
+ // If user provides a direct file path, only accept it when it matches the expected executable name.
91
+ if (path.basename(c) !== exe)
92
+ continue;
93
+ if (await validateBinary(c)) {
94
+ return c;
95
+ }
96
+ }
97
+ catch {
98
+ // ignore
99
+ }
100
+ }
101
+ return null;
102
+ }
103
+ /**
104
+ * Find package root by looking for package.json
105
+ */
106
+ function findPackageRoot() {
107
+ // Start from current module's directory
108
+ let dir = path.dirname(new URL(import.meta.url).pathname);
109
+ // On Windows, remove leading slash from /C:/...
110
+ if (process.platform === "win32" && dir.startsWith("/")) {
111
+ dir = dir.slice(1);
112
+ }
113
+ for (let i = 0; i < 10; i++) {
114
+ const pkgPath = path.join(dir, "package.json");
115
+ try {
116
+ if (fs.existsSync(pkgPath)) {
117
+ return dir;
118
+ }
119
+ }
120
+ catch {
121
+ // ignore
122
+ }
123
+ const parent = path.dirname(dir);
124
+ if (parent === dir)
125
+ break;
126
+ dir = parent;
127
+ }
128
+ return null;
129
+ }
130
+ /**
131
+ * Resolve embedded binary from bundled bin/ directory
132
+ */
133
+ async function resolveEmbeddedBinary(name, platform) {
134
+ const pkgRoot = findPackageRoot();
135
+ if (!pkgRoot)
136
+ return null;
137
+ const spec = ENGINE_SPECS[name];
138
+ const binDir = path.join(pkgRoot, "bin", `${name}_${platform}`);
139
+ const binPath = path.join(binDir, exeName(spec.assetPrefix));
140
+ try {
141
+ if (await exists(binPath) && await validateBinary(binPath)) {
142
+ return binPath;
143
+ }
144
+ }
145
+ catch {
146
+ // ignore
147
+ }
148
+ return null;
149
+ }
150
+ function makeBootstrapError(userMessage, internal) {
151
+ const debug = isDebugMode();
152
+ if (!debug)
153
+ return new Error(userMessage);
154
+ const detail = internal instanceof Error
155
+ ? internal.message
156
+ : typeof internal === "string"
157
+ ? internal
158
+ : internal
159
+ ? String(internal)
160
+ : "";
161
+ return new Error(detail ? `${userMessage}\n\n[debug] ${detail}` : userMessage);
162
+ }
163
+ function userMessageForBootstrapFailure(err) {
164
+ const msg = err instanceof Error ? err.message : String(err);
165
+ if (msg.includes("download_timeout") || msg.includes("AbortError")) {
166
+ return "다운로드 시간이 초과되었습니다. 인터넷 연결/방화벽 설정을 확인한 뒤 다시 시도해주세요.";
167
+ }
168
+ // P0-2: Better 404 error handling with recovery options
169
+ if (msg.includes("404") || msg.includes("no_matching_asset") || msg.includes("download_failed:404")) {
170
+ return `릴리스를 찾지 못했습니다.
171
+
172
+ 해결 방법:
173
+ 1. VIBECODE_BIN_PATH 환경변수로 로컬 바이너리 경로 지정
174
+ 2. VIBECODE_OFFLINE=1로 업데이트 스킵
175
+ 3. VIBECODE_DEBUG=1로 상세 로그 확인`;
176
+ }
177
+ if (msg.startsWith("download_failed") || msg.includes("fetch")) {
178
+ return "다운로드에 실패했습니다. 인터넷 연결/방화벽 설정을 확인한 뒤 다시 시도해주세요.";
179
+ }
180
+ if (msg.startsWith("sha_mismatch")) {
181
+ return "다운로드 검증에 실패했습니다(파일 손상 가능). 잠시 후 다시 시도해주세요.";
182
+ }
183
+ if (msg.startsWith("sha_missing_for_asset")) {
184
+ return "다운로드 검증 정보를 찾지 못했습니다. 잠시 후 다시 시도해주세요.";
185
+ }
186
+ if (msg.startsWith("bin_not_found_after_extract")) {
187
+ return "설치 파일을 찾지 못했습니다. 다시 시도해도 해결되지 않으면 지원팀에 문의해주세요.";
188
+ }
189
+ return msg;
190
+ }
191
+ // ============================================================
192
+ // SHA256 Verification
193
+ // ============================================================
194
+ function sha256File(filePath) {
195
+ return new Promise((resolve, reject) => {
196
+ const h = crypto.createHash("sha256");
197
+ const s = fs.createReadStream(filePath);
198
+ s.on("data", (d) => h.update(d));
199
+ s.on("error", reject);
200
+ s.on("end", () => resolve(h.digest("hex")));
201
+ });
202
+ }
203
+ // ============================================================
204
+ // Network Operations with Retry
205
+ // ============================================================
206
+ async function sleep(ms) {
207
+ return new Promise((resolve) => setTimeout(resolve, ms));
208
+ }
209
+ async function fetchWithRetry(url, retries = MAX_RETRIES) {
210
+ if (isOfflineMode()) {
211
+ throw makeBootstrapError("오프라인 모드에서는 다운로드를 진행할 수 없습니다. 인터넷을 연결하거나, 사전 설치된 도구 경로(VIBECODE_BIN_PATH)를 지정해주세요.");
212
+ }
213
+ let lastError = null;
214
+ for (let attempt = 1; attempt <= retries; attempt++) {
215
+ try {
216
+ const controller = new AbortController();
217
+ const timeoutId = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
218
+ const res = await fetch(url, { signal: controller.signal });
219
+ clearTimeout(timeoutId);
220
+ if (res.ok) {
221
+ return res;
222
+ }
223
+ // Server error (5xx) - retry
224
+ if (res.status >= 500) {
225
+ lastError = new Error(`server_error:${res.status}`);
226
+ if (attempt < retries) {
227
+ await sleep(RETRY_DELAY_MS * attempt);
228
+ continue;
229
+ }
230
+ }
231
+ // Client error (4xx) - don't retry
232
+ throw new Error(`download_failed:${res.status}`);
233
+ }
234
+ catch (e) {
235
+ lastError = e instanceof Error ? e : new Error(String(e));
236
+ // Abort error (timeout)
237
+ if (lastError.name === "AbortError") {
238
+ lastError = new Error("download_timeout");
239
+ }
240
+ // Network error - retry
241
+ if (attempt < retries && lastError.message.includes("fetch")) {
242
+ await sleep(RETRY_DELAY_MS * attempt);
243
+ continue;
244
+ }
245
+ if (attempt === retries) {
246
+ throw lastError;
247
+ }
248
+ }
249
+ }
250
+ throw lastError || new Error("download_failed:unknown");
251
+ }
252
+ async function download(url, outPath) {
253
+ const res = await fetchWithRetry(url);
254
+ await fs.promises.mkdir(path.dirname(outPath), { recursive: true });
255
+ const buf = Buffer.from(await res.arrayBuffer());
256
+ await fs.promises.writeFile(outPath, buf);
257
+ }
258
+ function releaseBaseUrl(repo, version) {
259
+ return `https://github.com/${repo}/releases/download/v${version}`;
260
+ }
261
+ async function readShaSums(repo, version, shaAsset) {
262
+ const url = `${releaseBaseUrl(repo, version)}/${shaAsset}`;
263
+ return readShaSumsFromUrl(url);
264
+ }
265
+ async function discoverRelease(repo, version) {
266
+ const apiBase = `https://api.github.com/repos/${repo}/releases`;
267
+ // Support both individual version tags and combined engine release tag
268
+ const tags = [`v${version}`, version, `release-${version}`, `engines-v1.0.0`];
269
+ if (isDebugMode()) {
270
+ console.log(`[debug] Discovering release for ${repo}@${version}`);
271
+ }
272
+ for (const tag of tags) {
273
+ try {
274
+ const controller = new AbortController();
275
+ const timeoutId = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
276
+ const res = await fetch(`${apiBase}/tags/${encodeURIComponent(tag)}`, {
277
+ signal: controller.signal,
278
+ headers: {
279
+ "Accept": "application/vnd.github+json",
280
+ "User-Agent": "vibecode-installer",
281
+ },
282
+ });
283
+ clearTimeout(timeoutId);
284
+ if (res.ok) {
285
+ const release = await res.json();
286
+ if (isDebugMode()) {
287
+ console.log(`[debug] Found release: ${release.tag_name} with ${release.assets.length} assets`);
288
+ }
289
+ return release;
290
+ }
291
+ }
292
+ catch {
293
+ // try next tag format
294
+ }
295
+ }
296
+ // fallback: try latest release
297
+ try {
298
+ const controller = new AbortController();
299
+ const timeoutId = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
300
+ const res = await fetch(`${apiBase}/latest`, {
301
+ signal: controller.signal,
302
+ headers: {
303
+ "Accept": "application/vnd.github+json",
304
+ "User-Agent": "vibecode-installer",
305
+ },
306
+ });
307
+ clearTimeout(timeoutId);
308
+ if (res.ok) {
309
+ const release = await res.json();
310
+ if (isDebugMode()) {
311
+ console.log(`[debug] Using latest release: ${release.tag_name}`);
312
+ }
313
+ return release;
314
+ }
315
+ }
316
+ catch {
317
+ // return null
318
+ }
319
+ return null;
320
+ }
321
+ function findAsset(release, name) {
322
+ return release.assets.find(a => a.name === name);
323
+ }
324
+ async function readShaSumsFromUrl(url) {
325
+ const res = await fetchWithRetry(url);
326
+ const text = await res.text();
327
+ const map = new Map();
328
+ for (const line of text.split("\n")) {
329
+ const t = line.trim();
330
+ if (!t)
331
+ continue;
332
+ // Format: "<sha256> <filename>"
333
+ const m = t.match(/^([a-fA-F0-9]{64})\s+(.+)$/);
334
+ if (!m)
335
+ continue;
336
+ map.set(m[2].trim(), m[1].toLowerCase());
337
+ }
338
+ return map;
339
+ }
340
+ // ============================================================
341
+ // Archive Operations
342
+ // ============================================================
343
+ async function extractTarGz(archivePath, destDir) {
344
+ await fs.promises.mkdir(destDir, { recursive: true });
345
+ // Windows 10+ and all unix have tar
346
+ await execFileAsync("tar", ["-xzf", archivePath, "-C", destDir]);
347
+ }
348
+ async function makeExecutable(binPath) {
349
+ if (process.platform === "win32")
350
+ return;
351
+ await fs.promises.chmod(binPath, 0o755);
352
+ }
353
+ // ============================================================
354
+ // Version Management
355
+ // ============================================================
356
+ async function getCurrentVersion(name) {
357
+ const vFile = versionFile(name);
358
+ try {
359
+ const content = await fs.promises.readFile(vFile, "utf-8");
360
+ return content.trim();
361
+ }
362
+ catch {
363
+ return null;
364
+ }
365
+ }
366
+ async function setCurrentVersion(name, version) {
367
+ const vFile = versionFile(name);
368
+ await fs.promises.mkdir(path.dirname(vFile), { recursive: true });
369
+ await fs.promises.writeFile(vFile, version, "utf-8");
370
+ }
371
+ /**
372
+ * Check if engine needs update
373
+ */
374
+ export async function needsUpdate(name) {
375
+ const spec = ENGINE_SPECS[name];
376
+ const current = await getCurrentVersion(name);
377
+ return current !== spec.version;
378
+ }
379
+ /**
380
+ * Check all engines for updates
381
+ */
382
+ export async function checkUpdates() {
383
+ const result = {};
384
+ for (const name of Object.keys(ENGINE_SPECS)) {
385
+ const spec = ENGINE_SPECS[name];
386
+ const current = await getCurrentVersion(name);
387
+ result[name] = {
388
+ current,
389
+ required: spec.version,
390
+ needsUpdate: current !== spec.version
391
+ };
392
+ }
393
+ return result;
394
+ }
395
+ // ============================================================
396
+ // Cache Validation
397
+ // ============================================================
398
+ /**
399
+ * Validate a cached binary by checking if it's executable
400
+ */
401
+ async function validateBinary(binPath) {
402
+ try {
403
+ // Check file exists
404
+ await fs.promises.access(binPath);
405
+ // Check file size is reasonable (at least 1KB)
406
+ const stat = await fs.promises.stat(binPath);
407
+ if (stat.size < 1024) {
408
+ return false;
409
+ }
410
+ // On unix, check if executable
411
+ if (process.platform !== "win32") {
412
+ try {
413
+ await fs.promises.access(binPath, fs.constants.X_OK);
414
+ }
415
+ catch {
416
+ return false;
417
+ }
418
+ }
419
+ return true;
420
+ }
421
+ catch {
422
+ return false;
423
+ }
424
+ }
425
+ /**
426
+ * Validate and repair cache for an engine
427
+ */
428
+ async function validateAndRepair(name, platform) {
429
+ const spec = ENGINE_SPECS[name];
430
+ const dir = engineDir(name, spec.version, platform);
431
+ const bin = path.join(dir, exeName(spec.assetPrefix));
432
+ const marker = path.join(dir, ".installed");
433
+ // Check if installed
434
+ if (!(await exists(marker))) {
435
+ return null;
436
+ }
437
+ // Validate binary
438
+ if (!(await validateBinary(bin))) {
439
+ // Corrupted - remove marker to trigger reinstall
440
+ await fs.promises.rm(marker, { force: true }).catch(() => { });
441
+ return null;
442
+ }
443
+ return bin;
444
+ }
445
+ /**
446
+ * Validate all cached engines
447
+ */
448
+ export async function validateCache() {
449
+ const platform = detectPlatform();
450
+ const result = {};
451
+ for (const name of Object.keys(ENGINE_SPECS)) {
452
+ const envBin = await resolveExternalBinary(name, platform);
453
+ if (envBin) {
454
+ result[name] = { valid: true, path: envBin };
455
+ continue;
456
+ }
457
+ const binPath = await validateAndRepair(name, platform);
458
+ result[name] = {
459
+ valid: binPath !== null,
460
+ path: binPath
461
+ };
462
+ }
463
+ return result;
464
+ }
465
+ /**
466
+ * Clear cache for an engine (for manual repair)
467
+ */
468
+ export async function clearCache(name) {
469
+ const enginesDir = path.join(cacheRoot(), "engines", name);
470
+ await fs.promises.rm(enginesDir, { recursive: true, force: true }).catch(() => { });
471
+ }
472
+ /**
473
+ * Clear all engine caches
474
+ */
475
+ export async function clearAllCaches() {
476
+ for (const name of Object.keys(ENGINE_SPECS)) {
477
+ await clearCache(name);
478
+ }
479
+ }
480
+ // ============================================================
481
+ // Main Installation Logic
482
+ // ============================================================
483
+ /**
484
+ * Ensure all engines are installed and return their paths
485
+ * Thread-safe via file lock
486
+ * Self-healing: validates cache, auto-updates, retries on failure
487
+ */
488
+ export async function ensureEngines() {
489
+ const platform = detectPlatform();
490
+ const lockPath = path.join(cacheRoot(), "locks", `install_${platform}.lock`);
491
+ return await withLock(lockPath, async () => {
492
+ const out = {};
493
+ for (const name of Object.keys(ENGINE_SPECS)) {
494
+ out[name] = await ensureOne(name, platform);
495
+ }
496
+ return out;
497
+ });
498
+ }
499
+ async function ensureOne(name, platform) {
500
+ const spec = ENGINE_SPECS[name];
501
+ const dir = engineDir(name, spec.version, platform);
502
+ const bin = path.join(dir, exeName(spec.assetPrefix));
503
+ const marker = path.join(dir, ".installed");
504
+ // 1. User-provided binary path (VIBECODE_BIN_PATH) - highest priority
505
+ const envBin = await resolveExternalBinary(name, platform);
506
+ if (envBin) {
507
+ return envBin;
508
+ }
509
+ // 2. Embedded binary (bundled in npm package) - for offline/air-gapped environments
510
+ const embeddedBin = await resolveEmbeddedBinary(name, platform);
511
+ if (embeddedBin) {
512
+ return embeddedBin;
513
+ }
514
+ // Check current version - auto-update if different
515
+ const currentVersion = await getCurrentVersion(name);
516
+ const needsVersionUpdate = currentVersion !== null && currentVersion !== spec.version;
517
+ // 3. Already cached and valid?
518
+ if (!needsVersionUpdate && (await exists(bin)) && (await exists(marker))) {
519
+ // Validate binary integrity
520
+ if (await validateBinary(bin)) {
521
+ return bin;
522
+ }
523
+ // Corrupted - remove marker to reinstall
524
+ await fs.promises.rm(marker, { force: true }).catch(() => { });
525
+ }
526
+ // 4. Download + verify + extract
527
+ if (isOfflineMode()) {
528
+ throw makeBootstrapError("오프라인 모드에서는 필요한 도구를 다운로드할 수 없습니다. 인터넷을 연결하거나, 사전 설치된 도구 경로(VIBECODE_BIN_PATH)를 지정해주세요.");
529
+ }
530
+ const asset = `${spec.assetPrefix}_${spec.version}_${platform}.tar.gz`;
531
+ const archivePath = path.join(dir, asset);
532
+ try {
533
+ // P0-2: Try GitHub API discovery first for better 404 tolerance
534
+ let assetUrl;
535
+ let shaMap;
536
+ const release = await discoverRelease(spec.repo, spec.version);
537
+ if (release) {
538
+ const assetInfo = findAsset(release, asset);
539
+ const shaInfo = findAsset(release, spec.shaSumsAsset);
540
+ if (assetInfo) {
541
+ assetUrl = assetInfo.browser_download_url;
542
+ // Read SHA from discovered release if available
543
+ if (shaInfo) {
544
+ shaMap = await readShaSumsFromUrl(shaInfo.browser_download_url);
545
+ }
546
+ else {
547
+ // Fallback to legacy URL for SHA
548
+ shaMap = await readShaSums(spec.repo, spec.version, spec.shaSumsAsset);
549
+ }
550
+ }
551
+ else {
552
+ throw new Error(`no_matching_asset:${asset} in release ${release.tag_name}`);
553
+ }
554
+ }
555
+ else {
556
+ // Fallback to legacy URL construction
557
+ if (isDebugMode()) {
558
+ console.log(`[debug] No release found via API, using legacy URL`);
559
+ }
560
+ assetUrl = `${releaseBaseUrl(spec.repo, spec.version)}/${asset}`;
561
+ shaMap = await readShaSums(spec.repo, spec.version, spec.shaSumsAsset);
562
+ }
563
+ const expected = shaMap.get(asset);
564
+ if (!expected)
565
+ throw new Error(`sha_missing_for_asset:${asset}`);
566
+ // Clean and prepare directory
567
+ await fs.promises.rm(dir, { recursive: true, force: true }).catch(() => { });
568
+ await fs.promises.mkdir(dir, { recursive: true });
569
+ // Download archive (with retry)
570
+ await download(assetUrl, archivePath);
571
+ // Verify SHA256
572
+ const got = (await sha256File(archivePath)).toLowerCase();
573
+ if (got !== expected)
574
+ throw new Error(`sha_mismatch:${asset}`);
575
+ // Extract
576
+ await extractTarGz(archivePath, dir);
577
+ // Find binary (may be at root or in subfolder)
578
+ const candidates = [
579
+ path.join(dir, exeName(spec.assetPrefix)),
580
+ path.join(dir, spec.assetPrefix, exeName(spec.assetPrefix))
581
+ ];
582
+ const found = await firstExisting(candidates);
583
+ if (!found)
584
+ throw new Error(`bin_not_found_after_extract:${spec.assetPrefix}`);
585
+ // Normalize location
586
+ if (found !== bin) {
587
+ await fs.promises.copyFile(found, bin);
588
+ }
589
+ await makeExecutable(bin);
590
+ await fs.promises.writeFile(marker, `ok ${new Date().toISOString()}\n`, "utf-8");
591
+ // Update version tracker
592
+ await setCurrentVersion(name, spec.version);
593
+ // Clean up archive to save space
594
+ await fs.promises.rm(archivePath, { force: true }).catch(() => { });
595
+ return bin;
596
+ }
597
+ catch (e) {
598
+ // Best-effort cleanup to avoid leaving partial installs behind.
599
+ await fs.promises.rm(dir, { recursive: true, force: true }).catch(() => { });
600
+ throw makeBootstrapError(userMessageForBootstrapFailure(e), e);
601
+ }
602
+ }
603
+ // ============================================================
604
+ // Utility Functions
605
+ // ============================================================
606
+ async function exists(p) {
607
+ try {
608
+ await fs.promises.access(p);
609
+ return true;
610
+ }
611
+ catch {
612
+ return false;
613
+ }
614
+ }
615
+ async function firstExisting(paths) {
616
+ for (const p of paths) {
617
+ if (await exists(p))
618
+ return p;
619
+ }
620
+ return null;
621
+ }
622
+ /**
623
+ * Get detailed health status of all engines
624
+ */
625
+ export async function getEngineHealth() {
626
+ const platform = detectPlatform();
627
+ const result = [];
628
+ for (const name of Object.keys(ENGINE_SPECS)) {
629
+ const spec = ENGINE_SPECS[name];
630
+ const dir = engineDir(name, spec.version, platform);
631
+ const bin = path.join(dir, exeName(spec.assetPrefix));
632
+ const marker = path.join(dir, ".installed");
633
+ const currentVersion = await getCurrentVersion(name);
634
+ let status;
635
+ let binPath = null;
636
+ // External binary override path (for air-gapped/offline environments)
637
+ const envBin = await resolveExternalBinary(name, platform);
638
+ if (envBin) {
639
+ status = "ok";
640
+ binPath = envBin;
641
+ result.push({
642
+ name,
643
+ version: spec.version,
644
+ currentVersion: spec.version,
645
+ path: binPath,
646
+ status
647
+ });
648
+ continue;
649
+ }
650
+ if (!(await exists(marker))) {
651
+ status = "missing";
652
+ }
653
+ else if (!(await validateBinary(bin))) {
654
+ status = "corrupted";
655
+ }
656
+ else if (currentVersion !== spec.version) {
657
+ status = "needs_update";
658
+ binPath = bin;
659
+ }
660
+ else {
661
+ status = "ok";
662
+ binPath = bin;
663
+ }
664
+ result.push({
665
+ name,
666
+ version: spec.version,
667
+ currentVersion,
668
+ path: binPath,
669
+ status
670
+ });
671
+ }
672
+ return result;
673
+ }
@@ -0,0 +1,37 @@
1
+ // adapters/mcp-ts/src/bootstrap/lock.ts
2
+ // Process lock to prevent concurrent installation
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ /**
6
+ * Execute function with exclusive file lock
7
+ * Prevents multiple MCP tool calls from racing on installation
8
+ */
9
+ export async function withLock(lockPath, fn) {
10
+ await fs.promises.mkdir(path.dirname(lockPath), { recursive: true });
11
+ // Best-effort exclusive lock (cross-platform)
12
+ const handle = await fs.promises.open(lockPath, "wx").catch(() => null);
13
+ if (!handle) {
14
+ // Another process holds the lock - wait and retry
15
+ for (let i = 0; i < 80; i++) {
16
+ await new Promise((r) => setTimeout(r, 250));
17
+ const h2 = await fs.promises.open(lockPath, "wx").catch(() => null);
18
+ if (h2) {
19
+ try {
20
+ return await fn();
21
+ }
22
+ finally {
23
+ await h2.close().catch(() => { });
24
+ await fs.promises.unlink(lockPath).catch(() => { });
25
+ }
26
+ }
27
+ }
28
+ throw new Error("lock_timeout");
29
+ }
30
+ try {
31
+ return await fn();
32
+ }
33
+ finally {
34
+ await handle.close().catch(() => { });
35
+ await fs.promises.unlink(lockPath).catch(() => { });
36
+ }
37
+ }