@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.
- package/LICENSE +21 -0
- package/README.md +269 -0
- package/build/auth/gate.js +225 -0
- package/build/auth/index.js +55 -0
- package/build/auth/public_key.js +27 -0
- package/build/auth/token_cache.js +122 -0
- package/build/auth/token_verifier.js +103 -0
- package/build/bootstrap/doctor.js +115 -0
- package/build/bootstrap/installer.js +673 -0
- package/build/bootstrap/lock.js +37 -0
- package/build/bootstrap/platform.js +26 -0
- package/build/bootstrap/registry.js +37 -0
- package/build/cache/index.js +147 -0
- package/build/cli.js +101 -0
- package/build/contracts.js +22 -0
- package/build/control_plane/gate.js +161 -0
- package/build/control_plane/index.js +6 -0
- package/build/dx/activity.js +139 -0
- package/build/engine.js +106 -0
- package/build/errors.js +171 -0
- package/build/generated/activate_input.js +2 -0
- package/build/generated/activate_output.js +57 -0
- package/build/generated/advisory_review_input.js +2 -0
- package/build/generated/advisory_review_output.js +35 -0
- package/build/generated/auth_token_file.js +2 -0
- package/build/generated/briefing_input.js +2 -0
- package/build/generated/briefing_output.js +2 -0
- package/build/generated/clinic_bridge_file.js +13 -0
- package/build/generated/contracts_bundle_info.js +5 -0
- package/build/generated/create_work_order_input.js +2 -0
- package/build/generated/create_work_order_output.js +2 -0
- package/build/generated/current_work_order_file.js +2 -0
- package/build/generated/doctor_input.js +2 -0
- package/build/generated/doctor_output.js +24 -0
- package/build/generated/execution_result.js +2 -0
- package/build/generated/execution_task.js +2 -0
- package/build/generated/export_output_input.js +2 -0
- package/build/generated/export_output_output.js +2 -0
- package/build/generated/finalize_work_input.js +2 -0
- package/build/generated/finalize_work_output.js +2 -0
- package/build/generated/gate_input.js +2 -0
- package/build/generated/gate_output.js +2 -0
- package/build/generated/gate_result_v1.js +2 -0
- package/build/generated/get_decision_input.js +2 -0
- package/build/generated/get_decision_output.js +13 -0
- package/build/generated/handoff_to_clinic.js +2 -0
- package/build/generated/index.js +75 -0
- package/build/generated/inspect_code_input.js +2 -0
- package/build/generated/inspect_code_output.js +13 -0
- package/build/generated/memory_retrieve_output.js +2 -0
- package/build/generated/memory_state_file.js +2 -0
- package/build/generated/memory_status_input.js +2 -0
- package/build/generated/memory_status_output.js +13 -0
- package/build/generated/memory_sync_input.js +2 -0
- package/build/generated/memory_sync_output.js +13 -0
- package/build/generated/plugin_result.js +2 -0
- package/build/generated/react_perf_check_patterns_input.js +2 -0
- package/build/generated/react_perf_check_patterns_output.js +2 -0
- package/build/generated/react_perf_generate_report_input.js +2 -0
- package/build/generated/react_perf_generate_report_output.js +2 -0
- package/build/generated/repair_plan_input.js +2 -0
- package/build/generated/repair_plan_output.js +2 -0
- package/build/generated/run_app_input.js +2 -0
- package/build/generated/run_app_output.js +2 -0
- package/build/generated/run_state_file.js +13 -0
- package/build/generated/scaffold_input.js +2 -0
- package/build/generated/scaffold_output.js +2 -0
- package/build/generated/search_oss_input.js +2 -0
- package/build/generated/search_oss_output.js +2 -0
- package/build/generated/selection_validation_result.js +2 -0
- package/build/generated/signal_agent_input.js +2 -0
- package/build/generated/spec_high_ask_queue_items_file.js +2 -0
- package/build/generated/spec_high_clinic_bridge_output.js +2 -0
- package/build/generated/spec_high_decision_draft_output.js +2 -0
- package/build/generated/spec_high_validate_output.js +2 -0
- package/build/generated/status_input.js +2 -0
- package/build/generated/status_output.js +2 -0
- package/build/generated/submit_decision_input.js +2 -0
- package/build/generated/submit_decision_output.js +2 -0
- package/build/generated/tool_error_output.js +2 -0
- package/build/generated/undo_last_task_input.js +2 -0
- package/build/generated/undo_last_task_output.js +2 -0
- package/build/generated/update_input.js +2 -0
- package/build/generated/update_output.js +2 -0
- package/build/generated/vibe_pm_inspection_result.js +2 -0
- package/build/generated/vibe_pm_report_markdown.js +2 -0
- package/build/generated/vibe_pm_verdict.js +2 -0
- package/build/generated/vibe_repo_config.js +2 -0
- package/build/generated/vibecoding_helper_answer_output.js +2 -0
- package/build/generated/vibecoding_helper_one_loop_selection_output.js +2 -0
- package/build/generated/vibecoding_helper_show_ask_queue_output.js +2 -0
- package/build/generated/work_order_v1.js +2 -0
- package/build/generated/zoekt_evidence_input.js +2 -0
- package/build/generated/zoekt_evidence_output.js +2 -0
- package/build/index.js +111 -0
- package/build/legacy_alias.js +65 -0
- package/build/local-mode/bash.js +61 -0
- package/build/local-mode/config.js +171 -0
- package/build/local-mode/git.js +33 -0
- package/build/local-mode/init.js +110 -0
- package/build/local-mode/paths.js +24 -0
- package/build/local-mode/templates.js +856 -0
- package/build/local-mode/work-order.js +41 -0
- package/build/resources/index.js +246 -0
- package/build/security/input-validator.js +119 -0
- package/build/security/path-policy.js +289 -0
- package/build/security/sandbox.js +228 -0
- package/build/tools/react_perf/check_patterns.js +172 -0
- package/build/tools/react_perf/generate_report.js +337 -0
- package/build/tools/react_perf/index.js +119 -0
- package/build/tools/react_perf/rules/advanced.js +325 -0
- package/build/tools/react_perf/rules/async.js +104 -0
- package/build/tools/react_perf/rules/bundle.js +101 -0
- package/build/tools/react_perf/rules/client.js +186 -0
- package/build/tools/react_perf/rules/index.js +74 -0
- package/build/tools/react_perf/rules/js.js +148 -0
- package/build/tools/react_perf/rules/rendering.js +166 -0
- package/build/tools/react_perf/rules/rerender.js +161 -0
- package/build/tools/react_perf/rules/server.js +141 -0
- package/build/tools/react_perf/types.js +127 -0
- package/build/tools/vibe_pm/activate.js +102 -0
- package/build/tools/vibe_pm/advisory_review.js +77 -0
- package/build/tools/vibe_pm/briefing.js +178 -0
- package/build/tools/vibe_pm/context.js +439 -0
- package/build/tools/vibe_pm/create_work_order.js +271 -0
- package/build/tools/vibe_pm/doc_status_gate.js +370 -0
- package/build/tools/vibe_pm/doctor.js +262 -0
- package/build/tools/vibe_pm/entity_gate/preflight.js +78 -0
- package/build/tools/vibe_pm/export_output.js +135 -0
- package/build/tools/vibe_pm/finalize_work.js +393 -0
- package/build/tools/vibe_pm/gate.js +33 -0
- package/build/tools/vibe_pm/get_decision.js +281 -0
- package/build/tools/vibe_pm/index.js +593 -0
- package/build/tools/vibe_pm/inspect_code.js +828 -0
- package/build/tools/vibe_pm/intent/generator.js +294 -0
- package/build/tools/vibe_pm/intent/index.js +5 -0
- package/build/tools/vibe_pm/intent/prompt_density.js +227 -0
- package/build/tools/vibe_pm/intent/types.js +70 -0
- package/build/tools/vibe_pm/intent/verifier.js +237 -0
- package/build/tools/vibe_pm/kce/doc_usage.js +51 -0
- package/build/tools/vibe_pm/kce/on_finalize.js +11 -0
- package/build/tools/vibe_pm/kce/preflight.js +232 -0
- package/build/tools/vibe_pm/local_memory.js +26 -0
- package/build/tools/vibe_pm/memory_status.js +82 -0
- package/build/tools/vibe_pm/memory_sync.js +134 -0
- package/build/tools/vibe_pm/modules/decision_snapshot.js +29 -0
- package/build/tools/vibe_pm/modules/ensure.js +100 -0
- package/build/tools/vibe_pm/modules/fingerprint.js +30 -0
- package/build/tools/vibe_pm/modules/fix_dependencies.js +394 -0
- package/build/tools/vibe_pm/modules/planning_v1.js +110 -0
- package/build/tools/vibe_pm/modules/repo_context.js +56 -0
- package/build/tools/vibe_pm/modules/research_v1.js +114 -0
- package/build/tools/vibe_pm/modules/skills_v1.js +100 -0
- package/build/tools/vibe_pm/pm_language.js +222 -0
- package/build/tools/vibe_pm/repair_plan.js +199 -0
- package/build/tools/vibe_pm/run_app.js +597 -0
- package/build/tools/vibe_pm/run_app_podman.js +64 -0
- package/build/tools/vibe_pm/scaffold.js +550 -0
- package/build/tools/vibe_pm/search_oss.js +124 -0
- package/build/tools/vibe_pm/status.js +153 -0
- package/build/tools/vibe_pm/submit_decision.js +87 -0
- package/build/tools/vibe_pm/system_design/issue_mapping.js +47 -0
- package/build/tools/vibe_pm/system_design/rulebook.js +112 -0
- package/build/tools/vibe_pm/system_design/semgrep.js +132 -0
- package/build/tools/vibe_pm/types.js +229 -0
- package/build/tools/vibe_pm/undo_last_task.js +163 -0
- package/build/tools/vibe_pm/update.js +146 -0
- package/build/tools/vibe_pm/zoekt_evidence.js +96 -0
- package/build/tools.js +269 -0
- package/build/version-check.js +239 -0
- package/build/vibe-cli.js +631 -0
- 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
|
+
}
|