@vibecodetown/mcp-server 2.2.4 → 2.2.5

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.
@@ -437,6 +437,239 @@ export async function checkUpdates() {
437
437
  }
438
438
  return result;
439
439
  }
440
+ /**
441
+ * Parse engine version from asset filename
442
+ * Format: {prefix}_{version}_{platform}.tar.gz
443
+ * Example: spec-high_1.0.0_linux_x64.tar.gz
444
+ */
445
+ function parseEngineVersionFromAsset(assetName) {
446
+ const match = assetName.match(/^(.+?)_(\d+\.\d+\.\d+)_(.+)\.tar\.gz$/);
447
+ if (!match)
448
+ return null;
449
+ return {
450
+ prefix: match[1],
451
+ version: match[2],
452
+ platform: match[3],
453
+ };
454
+ }
455
+ /**
456
+ * Find the latest engines release from GitHub
457
+ */
458
+ async function discoverLatestEnginesRelease(repo) {
459
+ if (isOfflineMode())
460
+ return null;
461
+ const apiBase = `https://api.github.com/repos/${repo}/releases`;
462
+ try {
463
+ const controller = new AbortController();
464
+ const timeoutId = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
465
+ // Get all releases and find latest engines-v* tag
466
+ const res = await fetch(`${apiBase}?per_page=20`, {
467
+ signal: controller.signal,
468
+ headers: {
469
+ ...getAuthHeaders(),
470
+ "Accept": "application/vnd.github+json",
471
+ },
472
+ });
473
+ clearTimeout(timeoutId);
474
+ if (!res.ok)
475
+ return null;
476
+ const releases = await res.json();
477
+ // Find latest engines release
478
+ const enginesRelease = releases.find(r => r.tag_name.startsWith("engines-v"));
479
+ if (enginesRelease) {
480
+ if (isDebugMode()) {
481
+ console.log(`[debug] Found latest engines release: ${enginesRelease.tag_name}`);
482
+ }
483
+ return enginesRelease;
484
+ }
485
+ return null;
486
+ }
487
+ catch (e) {
488
+ if (isDebugMode()) {
489
+ console.log(`[debug] Failed to discover latest release: ${e}`);
490
+ }
491
+ return null;
492
+ }
493
+ }
494
+ /**
495
+ * Extract engine versions from release assets
496
+ */
497
+ function extractEngineVersionsFromRelease(release, platform) {
498
+ const versions = new Map();
499
+ for (const asset of release.assets) {
500
+ const parsed = parseEngineVersionFromAsset(asset.name);
501
+ if (!parsed || parsed.platform !== platform)
502
+ continue;
503
+ // Map asset prefix to engine name
504
+ const engineName = Object.keys(ENGINE_SPECS).find(name => ENGINE_SPECS[name].assetPrefix === parsed.prefix);
505
+ if (engineName) {
506
+ versions.set(engineName, {
507
+ name: engineName,
508
+ version: parsed.version,
509
+ assetName: asset.name,
510
+ });
511
+ }
512
+ }
513
+ return versions;
514
+ }
515
+ /**
516
+ * Check for remote updates from GitHub Releases
517
+ * This allows updates without npm package update
518
+ */
519
+ export async function checkRemoteUpdates() {
520
+ const platform = detectPlatform();
521
+ const result = {
522
+ available: false,
523
+ engines: [],
524
+ releaseTag: null,
525
+ };
526
+ // Use first engine's repo (all engines use same repo)
527
+ const repo = ENGINE_SPECS["spec-high"].repo;
528
+ const release = await discoverLatestEnginesRelease(repo);
529
+ if (!release) {
530
+ return result;
531
+ }
532
+ result.releaseTag = release.tag_name;
533
+ const remoteVersions = extractEngineVersionsFromRelease(release, platform);
534
+ for (const name of Object.keys(ENGINE_SPECS)) {
535
+ const currentVersion = await getCurrentVersion(name);
536
+ const remote = remoteVersions.get(name);
537
+ if (remote) {
538
+ const needsUpdate = currentVersion !== remote.version;
539
+ result.engines.push({
540
+ name,
541
+ currentVersion,
542
+ remoteVersion: remote.version,
543
+ needsUpdate,
544
+ });
545
+ if (needsUpdate) {
546
+ result.available = true;
547
+ }
548
+ }
549
+ }
550
+ return result;
551
+ }
552
+ /**
553
+ * Update engines from remote release (ignoring registry.ts versions)
554
+ */
555
+ export async function updateFromRemote(force = false) {
556
+ const platform = detectPlatform();
557
+ const repo = ENGINE_SPECS["spec-high"].repo;
558
+ const release = await discoverLatestEnginesRelease(repo);
559
+ const updated = [];
560
+ const failed = [];
561
+ if (!release) {
562
+ if (isDebugMode()) {
563
+ console.log("[debug] No remote release found");
564
+ }
565
+ return { updated, failed };
566
+ }
567
+ const remoteVersions = extractEngineVersionsFromRelease(release, platform);
568
+ for (const name of Object.keys(ENGINE_SPECS)) {
569
+ const remote = remoteVersions.get(name);
570
+ if (!remote)
571
+ continue;
572
+ const currentVersion = await getCurrentVersion(name);
573
+ const needsUpdate = force || currentVersion !== remote.version;
574
+ if (!needsUpdate)
575
+ continue;
576
+ try {
577
+ // Download and install from remote
578
+ await installEngineFromRelease(name, release, remote.version, platform);
579
+ updated.push(name);
580
+ if (isDebugMode()) {
581
+ console.log(`[debug] Updated ${name}: ${currentVersion} → ${remote.version}`);
582
+ }
583
+ }
584
+ catch (e) {
585
+ failed.push(name);
586
+ if (isDebugMode()) {
587
+ console.log(`[debug] Failed to update ${name}: ${e}`);
588
+ }
589
+ }
590
+ }
591
+ return { updated, failed };
592
+ }
593
+ /**
594
+ * Install a specific engine from a GitHub release
595
+ */
596
+ async function installEngineFromRelease(name, release, version, platform) {
597
+ const spec = ENGINE_SPECS[name];
598
+ const asset = `${spec.assetPrefix}_${version}_${platform}.tar.gz`;
599
+ const dir = engineDir(name, version, platform);
600
+ const bin = path.join(dir, exeName(spec.assetPrefix));
601
+ const marker = path.join(dir, ".installed");
602
+ const archivePath = path.join(dir, asset);
603
+ const assetInfo = findAsset(release, asset);
604
+ const shaInfo = findAsset(release, spec.shaSumsAsset);
605
+ if (!assetInfo) {
606
+ throw new Error(`Asset not found: ${asset}`);
607
+ }
608
+ // Read SHA256
609
+ let shaMap;
610
+ if (shaInfo) {
611
+ const token = getGitHubToken();
612
+ if (token) {
613
+ const res = await fetchWithRetry(shaInfo.url, MAX_RETRIES, {
614
+ "Accept": "application/octet-stream",
615
+ });
616
+ const text = await res.text();
617
+ shaMap = new Map();
618
+ for (const line of text.split("\n")) {
619
+ const t = line.trim();
620
+ if (!t)
621
+ continue;
622
+ const m = t.match(/^([a-fA-F0-9]{64})\s+(.+)$/);
623
+ if (m)
624
+ shaMap.set(m[2].trim(), m[1].toLowerCase());
625
+ }
626
+ }
627
+ else {
628
+ shaMap = await readShaSumsFromUrl(shaInfo.browser_download_url);
629
+ }
630
+ }
631
+ else {
632
+ throw new Error(`SHA256SUMS not found in release`);
633
+ }
634
+ const expected = shaMap.get(asset);
635
+ if (!expected)
636
+ throw new Error(`sha_missing_for_asset:${asset}`);
637
+ // Clean and prepare directory
638
+ await fs.promises.rm(dir, { recursive: true, force: true }).catch(() => { });
639
+ await fs.promises.mkdir(dir, { recursive: true });
640
+ // Download
641
+ if (getGitHubToken()) {
642
+ await downloadAsset(assetInfo, archivePath);
643
+ }
644
+ else {
645
+ await download(assetInfo.browser_download_url, archivePath);
646
+ }
647
+ // Verify SHA256
648
+ const got = (await sha256File(archivePath)).toLowerCase();
649
+ if (got !== expected)
650
+ throw new Error(`sha_mismatch:${asset}`);
651
+ // Extract
652
+ await extractTarGz(archivePath, dir);
653
+ // Find binary
654
+ const candidates = [
655
+ path.join(dir, exeName(spec.assetPrefix)),
656
+ path.join(dir, spec.assetPrefix, exeName(spec.assetPrefix))
657
+ ];
658
+ const found = await firstExisting(candidates);
659
+ if (!found)
660
+ throw new Error(`bin_not_found_after_extract:${spec.assetPrefix}`);
661
+ // Normalize location
662
+ if (found !== bin) {
663
+ await fs.promises.copyFile(found, bin);
664
+ }
665
+ await makeExecutable(bin);
666
+ await fs.promises.writeFile(marker, `ok ${new Date().toISOString()}\n`, "utf-8");
667
+ // Update version tracker
668
+ await setCurrentVersion(name, version);
669
+ // Clean up
670
+ await fs.promises.rm(archivePath, { force: true }).catch(() => { });
671
+ return bin;
672
+ }
440
673
  // ============================================================
441
674
  // Cache Validation
442
675
  // ============================================================
@@ -1,5 +1,5 @@
1
1
  // Auto-generated from schemas/contracts.version.json + schemas/contracts.lock.json
2
2
  // DO NOT EDIT MANUALLY - run scripts/generate-contracts.sh
3
- export const CONTRACTS_VERSION = "1.8.6";
4
- export const CONTRACTS_BUNDLE_SHA256 = "117ae96448d5d0bd988281ae7170facabc90c20da074d1d3e845c63cc734ba06";
3
+ export const CONTRACTS_VERSION = "1.8.7";
4
+ export const CONTRACTS_BUNDLE_SHA256 = "a1e258a6eda62a87093b91e7bc28e2cde414450a05c26f3ba52b0e6eaa0660e1";
5
5
  export const CONTRACTS_SCHEMA_COUNT = 103;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- export const DoctorOutputSchema = z.object({ "status": z.enum(["OK", "FIXED", "NEEDS_ATTENTION", "ERROR"]), "summary": z.string().min(1), "contracts": z.object({ "version": z.string().min(1), "bundle_sha256": z.string().min(1), "schema_count": z.number().int().gte(0) }).strict().optional(), "engines": z.array(z.object({ "name": z.string().min(1), "status": z.enum(["정상", "업데이트 필요", "설치 필요", "손상됨"]), "version": z.string().min(1), "current_version": z.union([z.string(), z.null()]) }).strict()), "actions_taken": z.array(z.string()).optional(), "local_memory": z.any().superRefine((x, ctx) => {
2
+ export const DoctorOutputSchema = z.object({ "status": z.enum(["OK", "FIXED", "NEEDS_ATTENTION", "ERROR"]), "summary": z.string().min(1), "contracts": z.object({ "version": z.string().min(1), "bundle_sha256": z.string().min(1), "schema_count": z.number().int().gte(0) }).strict().optional(), "engines": z.array(z.object({ "name": z.string().min(1), "status": z.enum(["정상", "업데이트 필요", "설치 필요", "손상됨"]), "version": z.string().min(1), "current_version": z.union([z.string(), z.null()]) }).strict()), "actions_taken": z.array(z.string()).optional(), "python_cli": z.object({ "status": z.enum(["정상", "오류"]), "message": z.string().min(1), "pythonpath_detected": z.union([z.string(), z.null()]) }).strict().optional(), "skills": z.object({ "status": z.enum(["OK", "WARN", "ERROR"]), "version": z.union([z.string(), z.null()]), "installed": z.number().int().gte(0), "total": z.number().int().gte(0) }).strict().optional(), "remote_updates": z.object({ "available": z.boolean(), "release_tag": z.union([z.string(), z.null()]), "updates": z.array(z.object({ "name": z.string().min(1), "current": z.union([z.string(), z.null()]), "remote": z.string().min(1) }).strict()) }).strict().optional(), "local_memory": z.any().superRefine((x, ctx) => {
3
3
  const schemas = [z.object({ "project_id": z.string().min(1), "status": z.enum(["READY", "NEEDS_SYNC", "OFFLINE_NO_INDEX", "ERROR"]), "summary": z.string().min(1), "docs_root": z.string().min(1), "persist_dir": z.string().min(1), "collection": z.string().min(1), "stats": z.object({ "document_count": z.number().int().gte(0), "chunk_count": z.number().int().gte(0), "last_sync_at": z.union([z.string(), z.null()]).optional() }).strict(), "embedding": z.object({ "backend": z.string().min(1), "model": z.union([z.string(), z.null()]).optional(), "ready": z.boolean() }).strict().optional(), "offline_mode": z.boolean(), "issues": z.array(z.string()), "next_action": z.any().superRefine((x, ctx) => {
4
4
  const schemas = [z.object({ "tool": z.enum(["vibe_pm.memory_sync", "vibe_pm.doctor"]), "reason": z.string().min(1) }).strict(), z.null()];
5
5
  const errors = schemas.reduce((errors, schema) => ((result) => result.error ? [...errors, result.error] : errors)(schema.safeParse(x)), []);
@@ -1,2 +1,2 @@
1
1
  import { z } from "zod";
2
- export const UpdateInputSchema = z.object({ "force": z.boolean().describe("캐시를 무시하고 강제로 재설치할지 여부 (기본: false)").default(false), "engines": z.array(z.string().min(1)).describe("업데이트할 엔진 목록 (미지정 시 전체)").optional() }).strict().describe("SSOT schema for vibe_pm.update MCP tool input");
2
+ export const UpdateInputSchema = z.object({ "target": z.enum(["engines", "npm", "local", "all"]).describe("업데이트 대상: engines(엔진 바이너리), npm(npm 패키지), local(로컬 git pull + build), all(전체)").default("engines"), "force": z.boolean().describe("캐시를 무시하고 강제로 재설치할지 여부 (기본: false)").default(false), "engines": z.array(z.string().min(1)).describe("업데이트할 엔진 목록 (미지정 시 전체, target=engines일 때만 적용)").optional(), "local_path": z.string().min(1).describe("로컬 개발 폴더 경로 (target=local/all일 때 사용, 미지정 시 VIBE_LOCAL_DEV_PATH 환경변수 사용)").optional() }).strict().describe("SSOT schema for vibe_pm.update MCP tool input");
@@ -1,2 +1,2 @@
1
1
  import { z } from "zod";
2
- export const UpdateOutputSchema = z.object({ "status": z.enum(["OK", "PARTIAL", "ERROR"]), "summary": z.string().min(1), "results": z.array(z.object({ "name": z.string().min(1), "action": z.enum(["updated", "skipped", "failed"]), "from_version": z.union([z.string(), z.null()]), "to_version": z.string().min(1), "message": z.string().min(1) }).strict()), "next_action": z.object({ "type": z.enum(["NONE", "RETRY", "CONTACT_SUPPORT"]), "message": z.string().min(1) }).strict() }).strict().describe("SSOT schema for vibe_pm.update MCP tool output");
2
+ export const UpdateOutputSchema = z.object({ "status": z.enum(["OK", "PARTIAL", "ERROR"]), "summary": z.string().min(1), "results": z.array(z.object({ "name": z.string().min(1), "action": z.enum(["updated", "skipped", "failed"]), "from_version": z.union([z.string(), z.null()]), "to_version": z.string().min(1), "message": z.string().min(1) }).strict()).describe("엔진 업데이트 결과 목록 (target=engines/all일 때)").optional(), "npm_result": z.object({ "action": z.enum(["updated", "skipped", "failed"]), "from_version": z.union([z.string(), z.null()]).optional(), "to_version": z.union([z.string(), z.null()]).optional(), "message": z.string().min(1) }).strict().describe("npm 패키지 업데이트 결과 (target=npm/all일 때)").optional(), "local_result": z.object({ "action": z.enum(["updated", "skipped", "failed"]), "path": z.string().min(1).optional(), "git_status": z.string().optional(), "build_status": z.string().optional(), "message": z.string().min(1) }).strict().describe("로컬 빌드 업데이트 결과 (target=local/all일 때)").optional(), "next_action": z.object({ "type": z.enum(["NONE", "RETRY", "RESTART_CLAUDE", "CONTACT_SUPPORT"]), "message": z.string().min(1) }).strict() }).strict().describe("SSOT schema for vibe_pm.update MCP tool output");
@@ -1,30 +1,192 @@
1
1
  // adapters/mcp-ts/src/tools/vibe_pm/update.ts
2
- // vibe_pm.update - Manual engine update
2
+ // vibe_pm.update - Unified update tool (engines + npm + local build)
3
3
  import { UpdateInputSchema } from "../../generated/update_input.js";
4
- import { ensureEngines, checkUpdates, clearCache, getEngineHealth } from "../../bootstrap/installer.js";
4
+ import { ensureEngines, checkUpdates, clearCache, getEngineHealth, checkRemoteUpdates, updateFromRemote } from "../../bootstrap/installer.js";
5
5
  import { ENGINE_SPECS } from "../../bootstrap/registry.js";
6
+ import { invokeCli } from "../../runtime/cli_invoker.js";
6
7
  // ============================================================
7
8
  // Input/Output Types
8
9
  // ============================================================
9
10
  export { UpdateInputSchema };
10
11
  // ============================================================
11
- // Update Implementation
12
+ // Helper Functions
12
13
  // ============================================================
13
14
  /**
14
- * vibe_pm.update - Manual engine update
15
- *
16
- * PM-friendly description:
17
- * 엔진 바이너리를 최신 버전으로 업데이트합니다.
15
+ * Update npm package (@vibecodetown/mcp-server)
18
16
  */
19
- export async function update(input) {
20
- const force = input.force ?? false;
21
- const targetEngines = input.engines ?? Object.keys(ENGINE_SPECS);
17
+ async function updateNpmPackage() {
18
+ const cwd = process.cwd();
19
+ try {
20
+ // Get current version
21
+ let fromVersion = null;
22
+ try {
23
+ const listResult = await invokeCli({
24
+ bin: "npm",
25
+ args: ["list", "@vibecodetown/mcp-server", "--json"],
26
+ cwd,
27
+ timeoutMs: 30000
28
+ });
29
+ if (listResult.exitCode === 0) {
30
+ const parsed = JSON.parse(listResult.stdout);
31
+ fromVersion = parsed.dependencies?.["@vibecodetown/mcp-server"]?.version ?? null;
32
+ }
33
+ }
34
+ catch {
35
+ // Not installed globally, might be using npx
36
+ }
37
+ // Clear npx cache and update
38
+ await invokeCli({
39
+ bin: "npm",
40
+ args: ["cache", "clean", "--force"],
41
+ cwd,
42
+ timeoutMs: 30000
43
+ });
44
+ // Try to update global package
45
+ try {
46
+ await invokeCli({
47
+ bin: "npm",
48
+ args: ["update", "-g", "@vibecodetown/mcp-server"],
49
+ cwd,
50
+ timeoutMs: 60000
51
+ });
52
+ }
53
+ catch {
54
+ // Global update failed, try clearing npx cache
55
+ }
56
+ // Get new version
57
+ let toVersion = null;
58
+ try {
59
+ const showResult = await invokeCli({
60
+ bin: "npm",
61
+ args: ["show", "@vibecodetown/mcp-server", "version"],
62
+ cwd,
63
+ timeoutMs: 30000
64
+ });
65
+ if (showResult.exitCode === 0) {
66
+ toVersion = showResult.stdout.trim();
67
+ }
68
+ }
69
+ catch {
70
+ // Could not fetch remote version
71
+ }
72
+ if (toVersion && fromVersion !== toVersion) {
73
+ return {
74
+ action: "updated",
75
+ from_version: fromVersion,
76
+ to_version: toVersion,
77
+ message: "npm 패키지 업데이트 완료 (npx 캐시 클리어됨)"
78
+ };
79
+ }
80
+ else if (toVersion) {
81
+ return {
82
+ action: "skipped",
83
+ from_version: fromVersion,
84
+ to_version: toVersion,
85
+ message: "이미 최신 버전입니다"
86
+ };
87
+ }
88
+ else {
89
+ return {
90
+ action: "failed",
91
+ from_version: fromVersion,
92
+ to_version: null,
93
+ message: "버전 확인 실패"
94
+ };
95
+ }
96
+ }
97
+ catch (e) {
98
+ return {
99
+ action: "failed",
100
+ from_version: null,
101
+ to_version: null,
102
+ message: e instanceof Error ? e.message : "npm 업데이트 실패"
103
+ };
104
+ }
105
+ }
106
+ /**
107
+ * Update local development build (git pull + npm run build)
108
+ */
109
+ async function updateLocalBuild(localPath) {
110
+ const result = {
111
+ action: "updated",
112
+ path: localPath,
113
+ git_status: "",
114
+ build_status: "",
115
+ message: ""
116
+ };
117
+ try {
118
+ // Step 1: git pull
119
+ const gitResult = await invokeCli({
120
+ bin: "git",
121
+ args: ["pull", "origin", "main"],
122
+ cwd: localPath,
123
+ timeoutMs: 60000
124
+ });
125
+ if (gitResult.exitCode !== 0) {
126
+ result.git_status = "failed";
127
+ result.action = "failed";
128
+ result.message = `git pull 실패: ${gitResult.stderr || gitResult.stdout}`;
129
+ return result;
130
+ }
131
+ result.git_status = gitResult.stdout.includes("Already up to date")
132
+ ? "already_up_to_date"
133
+ : "pulled";
134
+ // Step 2: npm run build (in adapters/mcp-ts)
135
+ const mcpTsPath = `${localPath}/adapters/mcp-ts`;
136
+ const buildResult = await invokeCli({
137
+ bin: "npm",
138
+ args: ["run", "build"],
139
+ cwd: mcpTsPath,
140
+ timeoutMs: 60000
141
+ });
142
+ if (buildResult.exitCode !== 0) {
143
+ result.build_status = "failed";
144
+ result.action = "failed";
145
+ result.message = `빌드 실패: ${buildResult.stderr || buildResult.stdout}`;
146
+ return result;
147
+ }
148
+ result.build_status = "success";
149
+ // Success
150
+ if (result.git_status === "already_up_to_date") {
151
+ result.action = "skipped";
152
+ result.message = "이미 최신 상태입니다 (빌드 갱신됨)";
153
+ }
154
+ else {
155
+ result.action = "updated";
156
+ result.message = "git pull + 빌드 완료";
157
+ }
158
+ return result;
159
+ }
160
+ catch (e) {
161
+ return {
162
+ action: "failed",
163
+ path: localPath,
164
+ git_status: "error",
165
+ build_status: "error",
166
+ message: e instanceof Error ? e.message : "로컬 업데이트 실패"
167
+ };
168
+ }
169
+ }
170
+ /**
171
+ * Update engines (existing logic extracted)
172
+ */
173
+ async function updateEngines(targetEngines, force) {
22
174
  const results = [];
23
175
  try {
24
- // Step 1: Check what needs updating
176
+ // Check for remote updates first (GitHub Releases)
177
+ const remoteUpdates = await checkRemoteUpdates();
178
+ const hasRemoteUpdates = remoteUpdates.available;
179
+ // Check local registry updates
25
180
  const updateStatus = await checkUpdates();
26
- // Step 2: Filter engines to update
181
+ // Determine what to update (prefer remote versions)
27
182
  const enginesToUpdate = [];
183
+ const remoteVersionMap = new Map();
184
+ // Build remote version map
185
+ for (const engine of remoteUpdates.engines) {
186
+ if (engine.needsUpdate) {
187
+ remoteVersionMap.set(engine.name, engine.remoteVersion);
188
+ }
189
+ }
28
190
  for (const name of targetEngines) {
29
191
  if (!(name in ENGINE_SPECS)) {
30
192
  results.push({
@@ -38,7 +200,9 @@ export async function update(input) {
38
200
  }
39
201
  const engineName = name;
40
202
  const status = updateStatus[engineName];
41
- if (force || status.needsUpdate) {
203
+ const remoteVersion = remoteVersionMap.get(engineName);
204
+ // Update if: force mode, local needs update, OR remote has newer version
205
+ if (force || status.needsUpdate || remoteVersion) {
42
206
  enginesToUpdate.push(engineName);
43
207
  }
44
208
  else {
@@ -51,84 +215,223 @@ export async function update(input) {
51
215
  });
52
216
  }
53
217
  }
54
- // Step 3: Clear cache if force mode
218
+ // Clear cache if force mode
55
219
  if (force) {
56
220
  for (const name of enginesToUpdate) {
57
221
  await clearCache(name);
58
222
  }
59
223
  }
60
- // Step 4: Update engines
224
+ // Update engines
61
225
  if (enginesToUpdate.length > 0) {
62
- // Run ensureEngines which will update anything that needs updating
63
- await ensureEngines();
64
- // Check results
65
- const newHealth = await getEngineHealth();
66
- for (const name of enginesToUpdate) {
67
- const health = newHealth.find((h) => h.name === name);
68
- const oldStatus = updateStatus[name];
69
- if (health?.status === "ok") {
70
- results.push({
71
- name,
72
- action: "updated",
73
- from_version: oldStatus.current,
74
- to_version: health.version,
75
- message: force ? "강제 재설치 완료" : "업데이트 완료"
76
- });
226
+ if (hasRemoteUpdates) {
227
+ const remoteResult = await updateFromRemote(force);
228
+ const newHealth = await getEngineHealth();
229
+ for (const name of enginesToUpdate) {
230
+ const health = newHealth.find((h) => h.name === name);
231
+ const oldStatus = updateStatus[name];
232
+ const remoteVersion = remoteVersionMap.get(name);
233
+ if (remoteResult.updated.includes(name)) {
234
+ results.push({
235
+ name,
236
+ action: "updated",
237
+ from_version: oldStatus.current,
238
+ to_version: remoteVersion ?? health?.version ?? oldStatus.required,
239
+ message: force ? "강제 재설치 완료 (원격)" : "업데이트 완료 (원격)"
240
+ });
241
+ }
242
+ else if (remoteResult.failed.includes(name)) {
243
+ results.push({
244
+ name,
245
+ action: "failed",
246
+ from_version: oldStatus.current,
247
+ to_version: remoteVersion ?? oldStatus.required,
248
+ message: "원격 업데이트 실패"
249
+ });
250
+ }
251
+ else if (health?.status === "ok") {
252
+ results.push({
253
+ name,
254
+ action: "skipped",
255
+ from_version: oldStatus.current,
256
+ to_version: health.version,
257
+ message: "이미 최신 버전입니다"
258
+ });
259
+ }
77
260
  }
78
- else {
79
- results.push({
80
- name,
81
- action: "failed",
82
- from_version: oldStatus.current,
83
- to_version: oldStatus.required,
84
- message: `업데이트 실패: ${health?.status ?? "unknown"}`
85
- });
261
+ }
262
+ else {
263
+ // Fallback to local registry update
264
+ await ensureEngines();
265
+ const newHealth = await getEngineHealth();
266
+ for (const name of enginesToUpdate) {
267
+ const health = newHealth.find((h) => h.name === name);
268
+ const oldStatus = updateStatus[name];
269
+ if (health?.status === "ok") {
270
+ results.push({
271
+ name,
272
+ action: "updated",
273
+ from_version: oldStatus.current,
274
+ to_version: health.version,
275
+ message: force ? "강제 재설치 완료" : "업데이트 완료"
276
+ });
277
+ }
278
+ else {
279
+ results.push({
280
+ name,
281
+ action: "failed",
282
+ from_version: oldStatus.current,
283
+ to_version: oldStatus.required,
284
+ message: `업데이트 실패: ${health?.status ?? "unknown"}`
285
+ });
286
+ }
86
287
  }
87
288
  }
88
289
  }
89
- // Step 5: Generate summary
90
- const updated = results.filter((r) => r.action === "updated").length;
91
- const skipped = results.filter((r) => r.action === "skipped").length;
92
- const failed = results.filter((r) => r.action === "failed").length;
290
+ return results;
291
+ }
292
+ catch (e) {
293
+ results.push({
294
+ name: "engines",
295
+ action: "failed",
296
+ from_version: null,
297
+ to_version: "unknown",
298
+ message: e instanceof Error ? e.message : "엔진 업데이트 실패"
299
+ });
300
+ return results;
301
+ }
302
+ }
303
+ // ============================================================
304
+ // Main Update Function
305
+ // ============================================================
306
+ /**
307
+ * vibe_pm.update - Unified update tool
308
+ *
309
+ * PM-friendly description:
310
+ * 엔진, npm 패키지, 로컬 빌드를 업데이트합니다.
311
+ *
312
+ * target options:
313
+ * - "engines": 엔진 바이너리만 업데이트 (기본값)
314
+ * - "npm": npm 패키지만 업데이트
315
+ * - "local": 로컬 개발 환경만 업데이트 (git pull + build)
316
+ * - "all": 전체 업데이트
317
+ */
318
+ export async function update(input) {
319
+ const target = input.target ?? "engines";
320
+ const force = input.force ?? false;
321
+ const targetEngines = input.engines ?? Object.keys(ENGINE_SPECS);
322
+ const localPath = input.local_path ?? process.env.VIBE_LOCAL_DEV_PATH;
323
+ let engineResults;
324
+ let npmResult;
325
+ let localResult;
326
+ let needsRestart = false;
327
+ try {
328
+ // Execute updates based on target
329
+ if (target === "engines" || target === "all") {
330
+ engineResults = await updateEngines(targetEngines, force);
331
+ }
332
+ if (target === "npm" || target === "all") {
333
+ npmResult = await updateNpmPackage();
334
+ if (npmResult.action === "updated") {
335
+ needsRestart = true;
336
+ }
337
+ }
338
+ if (target === "local" || target === "all") {
339
+ if (!localPath) {
340
+ localResult = {
341
+ action: "failed",
342
+ path: "",
343
+ git_status: "error",
344
+ build_status: "error",
345
+ message: "로컬 경로가 지정되지 않았습니다. local_path 파라미터 또는 VIBE_LOCAL_DEV_PATH 환경변수를 설정하세요."
346
+ };
347
+ }
348
+ else {
349
+ localResult = await updateLocalBuild(localPath);
350
+ if (localResult.action === "updated") {
351
+ needsRestart = true;
352
+ }
353
+ }
354
+ }
355
+ // Generate summary
356
+ const summaryParts = [];
357
+ let hasError = false;
358
+ let hasUpdate = false;
359
+ if (engineResults) {
360
+ const engineUpdated = engineResults.filter((r) => r.action === "updated").length;
361
+ const engineFailed = engineResults.filter((r) => r.action === "failed").length;
362
+ if (engineUpdated > 0) {
363
+ summaryParts.push(`엔진 ${engineUpdated}개 업데이트`);
364
+ hasUpdate = true;
365
+ }
366
+ if (engineFailed > 0) {
367
+ summaryParts.push(`엔진 ${engineFailed}개 실패`);
368
+ hasError = true;
369
+ }
370
+ }
371
+ if (npmResult) {
372
+ if (npmResult.action === "updated") {
373
+ summaryParts.push("npm 패키지 업데이트");
374
+ hasUpdate = true;
375
+ }
376
+ else if (npmResult.action === "failed") {
377
+ summaryParts.push("npm 업데이트 실패");
378
+ hasError = true;
379
+ }
380
+ }
381
+ if (localResult) {
382
+ if (localResult.action === "updated") {
383
+ summaryParts.push("로컬 빌드 업데이트");
384
+ hasUpdate = true;
385
+ }
386
+ else if (localResult.action === "failed") {
387
+ summaryParts.push("로컬 빌드 실패");
388
+ hasError = true;
389
+ }
390
+ }
391
+ // Determine status and next action
93
392
  let status;
94
- let summary;
95
393
  let nextAction;
96
- if (failed > 0 && updated === 0) {
394
+ if (hasError && !hasUpdate) {
97
395
  status = "ERROR";
98
- summary = `업데이트 실패: ${failed}개 엔진`;
99
396
  nextAction = {
100
397
  type: "RETRY",
101
- message: "force: true 옵션으로 다시 시도하거나, 네트워크 연결을 확인하세요."
398
+ message: "업데이트에 실패했습니다. force: true 옵션으로 다시 시도하세요."
102
399
  };
103
400
  }
104
- else if (failed > 0) {
401
+ else if (hasError) {
105
402
  status = "PARTIAL";
106
- summary = `부분 성공: ${updated}개 업데이트, ${failed}개 실패`;
107
403
  nextAction = {
108
- type: "RETRY",
109
- message: "실패한 엔진은 force: true 옵션으로 다시 시도하세요."
404
+ type: needsRestart ? "RESTART_CLAUDE" : "RETRY",
405
+ message: needsRestart
406
+ ? "일부 업데이트 완료. Claude Code를 재시작하세요."
407
+ : "일부 업데이트 실패. 실패 항목을 확인하세요."
110
408
  };
111
409
  }
112
- else if (updated > 0) {
410
+ else if (hasUpdate) {
113
411
  status = "OK";
114
- summary = `${updated}개 엔진 업데이트 완료`;
115
412
  nextAction = {
116
- type: "NONE",
117
- message: "모든 엔진이 최신 버전입니다."
413
+ type: needsRestart ? "RESTART_CLAUDE" : "NONE",
414
+ message: needsRestart
415
+ ? "업데이트 완료. Claude Code를 재시작하면 적용됩니다."
416
+ : "업데이트 완료."
118
417
  };
119
418
  }
120
419
  else {
121
420
  status = "OK";
122
- summary = "모든 엔진이 이미 최신 버전입니다";
123
421
  nextAction = {
124
422
  type: "NONE",
125
- message: "업데이트할 항목이 없습니다."
423
+ message: "모두 최신 상태입니다."
126
424
  };
127
425
  }
426
+ const summary = summaryParts.length > 0
427
+ ? summaryParts.join(", ")
428
+ : "업데이트할 항목이 없습니다";
128
429
  return {
129
430
  status,
130
431
  summary,
131
- results,
432
+ results: engineResults,
433
+ npm_result: npmResult,
434
+ local_result: localResult,
132
435
  next_action: nextAction
133
436
  };
134
437
  }
@@ -136,10 +439,12 @@ export async function update(input) {
136
439
  return {
137
440
  status: "ERROR",
138
441
  summary: e instanceof Error ? e.message : "알 수 없는 오류",
139
- results,
442
+ results: engineResults,
443
+ npm_result: npmResult,
444
+ local_result: localResult,
140
445
  next_action: {
141
446
  type: "CONTACT_SUPPORT",
142
- message: "업데이트 중 오류가 발생했습니다. 네트워크 연결을 확인하고 다시 시도하세요."
447
+ message: "업데이트 중 오류가 발생했습니다."
143
448
  }
144
449
  };
145
450
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecodetown/mcp-server",
3
- "version": "2.2.4",
3
+ "version": "2.2.5",
4
4
  "type": "module",
5
5
  "description": "Vibe PM - AI Project Manager MCP Server for non-technical founders",
6
6
  "keywords": [