@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.
- package/build/bootstrap/installer.js +233 -0
- package/build/generated/contracts_bundle_info.js +2 -2
- package/build/generated/doctor_output.js +1 -1
- package/build/generated/update_input.js +1 -1
- package/build/generated/update_output.js +1 -1
- package/build/tools/vibe_pm/update.js +364 -59
- package/package.json +1 -1
|
@@ -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.
|
|
4
|
-
export const CONTRACTS_BUNDLE_SHA256 = "
|
|
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("업데이트할 엔진 목록 (미지정 시
|
|
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 -
|
|
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
|
-
//
|
|
12
|
+
// Helper Functions
|
|
12
13
|
// ============================================================
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* PM-friendly description:
|
|
17
|
-
* 엔진 바이너리를 최신 버전으로 업데이트합니다.
|
|
15
|
+
* Update npm package (@vibecodetown/mcp-server)
|
|
18
16
|
*/
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
224
|
+
// Update engines
|
|
61
225
|
if (enginesToUpdate.length > 0) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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 (
|
|
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 (
|
|
401
|
+
else if (hasError) {
|
|
105
402
|
status = "PARTIAL";
|
|
106
|
-
summary = `부분 성공: ${updated}개 업데이트, ${failed}개 실패`;
|
|
107
403
|
nextAction = {
|
|
108
|
-
type: "RETRY",
|
|
109
|
-
message:
|
|
404
|
+
type: needsRestart ? "RESTART_CLAUDE" : "RETRY",
|
|
405
|
+
message: needsRestart
|
|
406
|
+
? "일부 업데이트 완료. Claude Code를 재시작하세요."
|
|
407
|
+
: "일부 업데이트 실패. 실패 항목을 확인하세요."
|
|
110
408
|
};
|
|
111
409
|
}
|
|
112
|
-
else if (
|
|
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
|
}
|