@vibecodetown/mcp-server 2.2.4 → 2.2.6

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,10 +1,12 @@
1
1
  // adapters/mcp-ts/src/local-mode/version-lock.ts
2
2
  // Version lock management for unified installation
3
3
  import * as fs from "node:fs";
4
+ import * as path from "node:path";
4
5
  import { z } from "zod";
5
6
  import { getVibeRepoPaths } from "./paths.js";
6
7
  export const VersionLockSchema = z.object({
7
8
  schema_version: z.literal(1),
9
+ repo_root: z.string().min(1).optional(),
8
10
  created_at: z.string(),
9
11
  updated_at: z.string(),
10
12
  cli: z.object({
@@ -18,6 +20,7 @@ export const VersionLockSchema = z.object({
18
20
  });
19
21
  /**
20
22
  * Read version lock file from repo
23
+ * Auto-injects repo_root if missing (migration for existing files)
21
24
  */
22
25
  export function readVersionLock(repoRoot) {
23
26
  const paths = getVibeRepoPaths(repoRoot);
@@ -25,6 +28,10 @@ export function readVersionLock(repoRoot) {
25
28
  return null;
26
29
  try {
27
30
  const data = JSON.parse(fs.readFileSync(paths.versionLockFile, "utf-8"));
31
+ // Auto-inject repo_root if missing (migration for existing lock files)
32
+ if (!data.repo_root) {
33
+ data.repo_root = path.resolve(repoRoot);
34
+ }
28
35
  return VersionLockSchema.parse(data);
29
36
  }
30
37
  catch {
@@ -38,8 +45,11 @@ export function writeVersionLock(repoRoot, cliVersion, engines) {
38
45
  const paths = getVibeRepoPaths(repoRoot);
39
46
  const now = new Date().toISOString();
40
47
  const existing = readVersionLock(repoRoot);
48
+ // Store absolute path for later reference (e.g., vibe_pm.update with target=local)
49
+ const absoluteRepoRoot = path.resolve(repoRoot);
41
50
  const lock = {
42
51
  schema_version: 1,
52
+ repo_root: absoluteRepoRoot,
43
53
  created_at: existing?.created_at ?? now,
44
54
  updated_at: now,
45
55
  cli: { name: "@vibecode/mcp-server", version: cliVersion },
@@ -1,30 +1,193 @@
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";
7
+ import { readVersionLock } from "../../local-mode/version-lock.js";
6
8
  // ============================================================
7
9
  // Input/Output Types
8
10
  // ============================================================
9
11
  export { UpdateInputSchema };
10
12
  // ============================================================
11
- // Update Implementation
13
+ // Helper Functions
12
14
  // ============================================================
13
15
  /**
14
- * vibe_pm.update - Manual engine update
15
- *
16
- * PM-friendly description:
17
- * 엔진 바이너리를 최신 버전으로 업데이트합니다.
16
+ * Update npm package (@vibecodetown/mcp-server)
18
17
  */
19
- export async function update(input) {
20
- const force = input.force ?? false;
21
- const targetEngines = input.engines ?? Object.keys(ENGINE_SPECS);
18
+ async function updateNpmPackage() {
19
+ const cwd = process.cwd();
20
+ try {
21
+ // Get current version
22
+ let fromVersion = null;
23
+ try {
24
+ const listResult = await invokeCli({
25
+ bin: "npm",
26
+ args: ["list", "@vibecodetown/mcp-server", "--json"],
27
+ cwd,
28
+ timeoutMs: 30000
29
+ });
30
+ if (listResult.exitCode === 0) {
31
+ const parsed = JSON.parse(listResult.stdout);
32
+ fromVersion = parsed.dependencies?.["@vibecodetown/mcp-server"]?.version ?? null;
33
+ }
34
+ }
35
+ catch {
36
+ // Not installed globally, might be using npx
37
+ }
38
+ // Clear npx cache and update
39
+ await invokeCli({
40
+ bin: "npm",
41
+ args: ["cache", "clean", "--force"],
42
+ cwd,
43
+ timeoutMs: 30000
44
+ });
45
+ // Try to update global package
46
+ try {
47
+ await invokeCli({
48
+ bin: "npm",
49
+ args: ["update", "-g", "@vibecodetown/mcp-server"],
50
+ cwd,
51
+ timeoutMs: 60000
52
+ });
53
+ }
54
+ catch {
55
+ // Global update failed, try clearing npx cache
56
+ }
57
+ // Get new version
58
+ let toVersion = null;
59
+ try {
60
+ const showResult = await invokeCli({
61
+ bin: "npm",
62
+ args: ["show", "@vibecodetown/mcp-server", "version"],
63
+ cwd,
64
+ timeoutMs: 30000
65
+ });
66
+ if (showResult.exitCode === 0) {
67
+ toVersion = showResult.stdout.trim();
68
+ }
69
+ }
70
+ catch {
71
+ // Could not fetch remote version
72
+ }
73
+ if (toVersion && fromVersion !== toVersion) {
74
+ return {
75
+ action: "updated",
76
+ from_version: fromVersion,
77
+ to_version: toVersion,
78
+ message: "npm 패키지 업데이트 완료 (npx 캐시 클리어됨)"
79
+ };
80
+ }
81
+ else if (toVersion) {
82
+ return {
83
+ action: "skipped",
84
+ from_version: fromVersion,
85
+ to_version: toVersion,
86
+ message: "이미 최신 버전입니다"
87
+ };
88
+ }
89
+ else {
90
+ return {
91
+ action: "failed",
92
+ from_version: fromVersion,
93
+ to_version: null,
94
+ message: "버전 확인 실패"
95
+ };
96
+ }
97
+ }
98
+ catch (e) {
99
+ return {
100
+ action: "failed",
101
+ from_version: null,
102
+ to_version: null,
103
+ message: e instanceof Error ? e.message : "npm 업데이트 실패"
104
+ };
105
+ }
106
+ }
107
+ /**
108
+ * Update local development build (git pull + npm run build)
109
+ */
110
+ async function updateLocalBuild(localPath) {
111
+ const result = {
112
+ action: "updated",
113
+ path: localPath,
114
+ git_status: "",
115
+ build_status: "",
116
+ message: ""
117
+ };
118
+ try {
119
+ // Step 1: git pull
120
+ const gitResult = await invokeCli({
121
+ bin: "git",
122
+ args: ["pull", "origin", "main"],
123
+ cwd: localPath,
124
+ timeoutMs: 60000
125
+ });
126
+ if (gitResult.exitCode !== 0) {
127
+ result.git_status = "failed";
128
+ result.action = "failed";
129
+ result.message = `git pull 실패: ${gitResult.stderr || gitResult.stdout}`;
130
+ return result;
131
+ }
132
+ result.git_status = gitResult.stdout.includes("Already up to date")
133
+ ? "already_up_to_date"
134
+ : "pulled";
135
+ // Step 2: npm run build (in adapters/mcp-ts)
136
+ const mcpTsPath = `${localPath}/adapters/mcp-ts`;
137
+ const buildResult = await invokeCli({
138
+ bin: "npm",
139
+ args: ["run", "build"],
140
+ cwd: mcpTsPath,
141
+ timeoutMs: 60000
142
+ });
143
+ if (buildResult.exitCode !== 0) {
144
+ result.build_status = "failed";
145
+ result.action = "failed";
146
+ result.message = `빌드 실패: ${buildResult.stderr || buildResult.stdout}`;
147
+ return result;
148
+ }
149
+ result.build_status = "success";
150
+ // Success
151
+ if (result.git_status === "already_up_to_date") {
152
+ result.action = "skipped";
153
+ result.message = "이미 최신 상태입니다 (빌드 갱신됨)";
154
+ }
155
+ else {
156
+ result.action = "updated";
157
+ result.message = "git pull + 빌드 완료";
158
+ }
159
+ return result;
160
+ }
161
+ catch (e) {
162
+ return {
163
+ action: "failed",
164
+ path: localPath,
165
+ git_status: "error",
166
+ build_status: "error",
167
+ message: e instanceof Error ? e.message : "로컬 업데이트 실패"
168
+ };
169
+ }
170
+ }
171
+ /**
172
+ * Update engines (existing logic extracted)
173
+ */
174
+ async function updateEngines(targetEngines, force) {
22
175
  const results = [];
23
176
  try {
24
- // Step 1: Check what needs updating
177
+ // Check for remote updates first (GitHub Releases)
178
+ const remoteUpdates = await checkRemoteUpdates();
179
+ const hasRemoteUpdates = remoteUpdates.available;
180
+ // Check local registry updates
25
181
  const updateStatus = await checkUpdates();
26
- // Step 2: Filter engines to update
182
+ // Determine what to update (prefer remote versions)
27
183
  const enginesToUpdate = [];
184
+ const remoteVersionMap = new Map();
185
+ // Build remote version map
186
+ for (const engine of remoteUpdates.engines) {
187
+ if (engine.needsUpdate) {
188
+ remoteVersionMap.set(engine.name, engine.remoteVersion);
189
+ }
190
+ }
28
191
  for (const name of targetEngines) {
29
192
  if (!(name in ENGINE_SPECS)) {
30
193
  results.push({
@@ -38,7 +201,9 @@ export async function update(input) {
38
201
  }
39
202
  const engineName = name;
40
203
  const status = updateStatus[engineName];
41
- if (force || status.needsUpdate) {
204
+ const remoteVersion = remoteVersionMap.get(engineName);
205
+ // Update if: force mode, local needs update, OR remote has newer version
206
+ if (force || status.needsUpdate || remoteVersion) {
42
207
  enginesToUpdate.push(engineName);
43
208
  }
44
209
  else {
@@ -51,84 +216,230 @@ export async function update(input) {
51
216
  });
52
217
  }
53
218
  }
54
- // Step 3: Clear cache if force mode
219
+ // Clear cache if force mode
55
220
  if (force) {
56
221
  for (const name of enginesToUpdate) {
57
222
  await clearCache(name);
58
223
  }
59
224
  }
60
- // Step 4: Update engines
225
+ // Update engines
61
226
  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
- });
227
+ if (hasRemoteUpdates) {
228
+ const remoteResult = await updateFromRemote(force);
229
+ const newHealth = await getEngineHealth();
230
+ for (const name of enginesToUpdate) {
231
+ const health = newHealth.find((h) => h.name === name);
232
+ const oldStatus = updateStatus[name];
233
+ const remoteVersion = remoteVersionMap.get(name);
234
+ if (remoteResult.updated.includes(name)) {
235
+ results.push({
236
+ name,
237
+ action: "updated",
238
+ from_version: oldStatus.current,
239
+ to_version: remoteVersion ?? health?.version ?? oldStatus.required,
240
+ message: force ? "강제 재설치 완료 (원격)" : "업데이트 완료 (원격)"
241
+ });
242
+ }
243
+ else if (remoteResult.failed.includes(name)) {
244
+ results.push({
245
+ name,
246
+ action: "failed",
247
+ from_version: oldStatus.current,
248
+ to_version: remoteVersion ?? oldStatus.required,
249
+ message: "원격 업데이트 실패"
250
+ });
251
+ }
252
+ else if (health?.status === "ok") {
253
+ results.push({
254
+ name,
255
+ action: "skipped",
256
+ from_version: oldStatus.current,
257
+ to_version: health.version,
258
+ message: "이미 최신 버전입니다"
259
+ });
260
+ }
261
+ }
262
+ }
263
+ else {
264
+ // Fallback to local registry update
265
+ await ensureEngines();
266
+ const newHealth = await getEngineHealth();
267
+ for (const name of enginesToUpdate) {
268
+ const health = newHealth.find((h) => h.name === name);
269
+ const oldStatus = updateStatus[name];
270
+ if (health?.status === "ok") {
271
+ results.push({
272
+ name,
273
+ action: "updated",
274
+ from_version: oldStatus.current,
275
+ to_version: health.version,
276
+ message: force ? "강제 재설치 완료" : "업데이트 완료"
277
+ });
278
+ }
279
+ else {
280
+ results.push({
281
+ name,
282
+ action: "failed",
283
+ from_version: oldStatus.current,
284
+ to_version: oldStatus.required,
285
+ message: `업데이트 실패: ${health?.status ?? "unknown"}`
286
+ });
287
+ }
77
288
  }
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
- });
289
+ }
290
+ }
291
+ return results;
292
+ }
293
+ catch (e) {
294
+ results.push({
295
+ name: "engines",
296
+ action: "failed",
297
+ from_version: null,
298
+ to_version: "unknown",
299
+ message: e instanceof Error ? e.message : "엔진 업데이트 실패"
300
+ });
301
+ return results;
302
+ }
303
+ }
304
+ // ============================================================
305
+ // Main Update Function
306
+ // ============================================================
307
+ /**
308
+ * vibe_pm.update - Unified update tool
309
+ *
310
+ * PM-friendly description:
311
+ * 엔진, npm 패키지, 로컬 빌드를 업데이트합니다.
312
+ *
313
+ * target options:
314
+ * - "engines": 엔진 바이너리만 업데이트 (기본값)
315
+ * - "npm": npm 패키지만 업데이트
316
+ * - "local": 로컬 개발 환경만 업데이트 (git pull + build)
317
+ * - "all": 전체 업데이트
318
+ */
319
+ export async function update(input) {
320
+ const target = input.target ?? "engines";
321
+ const force = input.force ?? false;
322
+ const targetEngines = input.engines ?? Object.keys(ENGINE_SPECS);
323
+ // Resolve local path: input > env > version_lock.json
324
+ let localPath = input.local_path ?? process.env.VIBE_LOCAL_DEV_PATH;
325
+ if (!localPath && (target === "local" || target === "all")) {
326
+ const versionLock = readVersionLock(process.cwd());
327
+ if (versionLock?.repo_root) {
328
+ localPath = versionLock.repo_root;
329
+ }
330
+ }
331
+ let engineResults;
332
+ let npmResult;
333
+ let localResult;
334
+ let needsRestart = false;
335
+ try {
336
+ // Execute updates based on target
337
+ if (target === "engines" || target === "all") {
338
+ engineResults = await updateEngines(targetEngines, force);
339
+ }
340
+ if (target === "npm" || target === "all") {
341
+ npmResult = await updateNpmPackage();
342
+ if (npmResult.action === "updated") {
343
+ needsRestart = true;
344
+ }
345
+ }
346
+ if (target === "local" || target === "all") {
347
+ if (!localPath) {
348
+ localResult = {
349
+ action: "failed",
350
+ path: "(not configured)",
351
+ git_status: "error",
352
+ build_status: "error",
353
+ message: "로컬 경로가 지정되지 않았습니다. 'vibe setup'을 먼저 실행하거나, local_path 파라미터를 지정하세요."
354
+ };
355
+ }
356
+ else {
357
+ localResult = await updateLocalBuild(localPath);
358
+ if (localResult.action === "updated") {
359
+ needsRestart = true;
86
360
  }
87
361
  }
88
362
  }
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;
363
+ // Generate summary
364
+ const summaryParts = [];
365
+ let hasError = false;
366
+ let hasUpdate = false;
367
+ if (engineResults) {
368
+ const engineUpdated = engineResults.filter((r) => r.action === "updated").length;
369
+ const engineFailed = engineResults.filter((r) => r.action === "failed").length;
370
+ if (engineUpdated > 0) {
371
+ summaryParts.push(`엔진 ${engineUpdated}개 업데이트`);
372
+ hasUpdate = true;
373
+ }
374
+ if (engineFailed > 0) {
375
+ summaryParts.push(`엔진 ${engineFailed}개 실패`);
376
+ hasError = true;
377
+ }
378
+ }
379
+ if (npmResult) {
380
+ if (npmResult.action === "updated") {
381
+ summaryParts.push("npm 패키지 업데이트");
382
+ hasUpdate = true;
383
+ }
384
+ else if (npmResult.action === "failed") {
385
+ summaryParts.push("npm 업데이트 실패");
386
+ hasError = true;
387
+ }
388
+ }
389
+ if (localResult) {
390
+ if (localResult.action === "updated") {
391
+ summaryParts.push("로컬 빌드 업데이트");
392
+ hasUpdate = true;
393
+ }
394
+ else if (localResult.action === "failed") {
395
+ summaryParts.push("로컬 빌드 실패");
396
+ hasError = true;
397
+ }
398
+ }
399
+ // Determine status and next action
93
400
  let status;
94
- let summary;
95
401
  let nextAction;
96
- if (failed > 0 && updated === 0) {
402
+ if (hasError && !hasUpdate) {
97
403
  status = "ERROR";
98
- summary = `업데이트 실패: ${failed}개 엔진`;
99
404
  nextAction = {
100
405
  type: "RETRY",
101
- message: "force: true 옵션으로 다시 시도하거나, 네트워크 연결을 확인하세요."
406
+ message: "업데이트에 실패했습니다. force: true 옵션으로 다시 시도하세요."
102
407
  };
103
408
  }
104
- else if (failed > 0) {
409
+ else if (hasError) {
105
410
  status = "PARTIAL";
106
- summary = `부분 성공: ${updated}개 업데이트, ${failed}개 실패`;
107
411
  nextAction = {
108
- type: "RETRY",
109
- message: "실패한 엔진은 force: true 옵션으로 다시 시도하세요."
412
+ type: needsRestart ? "RESTART_CLAUDE" : "RETRY",
413
+ message: needsRestart
414
+ ? "일부 업데이트 완료. Claude Code를 재시작하세요."
415
+ : "일부 업데이트 실패. 실패 항목을 확인하세요."
110
416
  };
111
417
  }
112
- else if (updated > 0) {
418
+ else if (hasUpdate) {
113
419
  status = "OK";
114
- summary = `${updated}개 엔진 업데이트 완료`;
115
420
  nextAction = {
116
- type: "NONE",
117
- message: "모든 엔진이 최신 버전입니다."
421
+ type: needsRestart ? "RESTART_CLAUDE" : "NONE",
422
+ message: needsRestart
423
+ ? "업데이트 완료. Claude Code를 재시작하면 적용됩니다."
424
+ : "업데이트 완료."
118
425
  };
119
426
  }
120
427
  else {
121
428
  status = "OK";
122
- summary = "모든 엔진이 이미 최신 버전입니다";
123
429
  nextAction = {
124
430
  type: "NONE",
125
- message: "업데이트할 항목이 없습니다."
431
+ message: "모두 최신 상태입니다."
126
432
  };
127
433
  }
434
+ const summary = summaryParts.length > 0
435
+ ? summaryParts.join(", ")
436
+ : "업데이트할 항목이 없습니다";
128
437
  return {
129
438
  status,
130
439
  summary,
131
- results,
440
+ results: engineResults,
441
+ npm_result: npmResult,
442
+ local_result: localResult,
132
443
  next_action: nextAction
133
444
  };
134
445
  }
@@ -136,10 +447,12 @@ export async function update(input) {
136
447
  return {
137
448
  status: "ERROR",
138
449
  summary: e instanceof Error ? e.message : "알 수 없는 오류",
139
- results,
450
+ results: engineResults,
451
+ npm_result: npmResult,
452
+ local_result: localResult,
140
453
  next_action: {
141
454
  type: "CONTACT_SUPPORT",
142
- message: "업데이트 중 오류가 발생했습니다. 네트워크 연결을 확인하고 다시 시도하세요."
455
+ message: "업데이트 중 오류가 발생했습니다."
143
456
  }
144
457
  };
145
458
  }
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.6",
4
4
  "type": "module",
5
5
  "description": "Vibe PM - AI Project Manager MCP Server for non-technical founders",
6
6
  "keywords": [