mcpman 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/dist/index.cjs +638 -431
- package/dist/index.js +618 -417
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
} from "./chunk-QY22QTBR.js";
|
|
5
5
|
import {
|
|
6
6
|
getMasterPassword,
|
|
7
|
+
getSecretsForServer,
|
|
7
8
|
listSecrets,
|
|
8
9
|
removeSecret,
|
|
9
10
|
setSecret
|
|
@@ -14,6 +15,7 @@ import { defineCommand as defineCommand10, runMain } from "citty";
|
|
|
14
15
|
|
|
15
16
|
// src/commands/audit.ts
|
|
16
17
|
import { defineCommand } from "citty";
|
|
18
|
+
import * as p from "@clack/prompts";
|
|
17
19
|
import pc from "picocolors";
|
|
18
20
|
import { createSpinner } from "nanospinner";
|
|
19
21
|
|
|
@@ -209,17 +211,324 @@ async function scanAllServers(servers, concurrency = 3) {
|
|
|
209
211
|
const results = [];
|
|
210
212
|
const executing = /* @__PURE__ */ new Set();
|
|
211
213
|
for (const [name, entry] of entries) {
|
|
212
|
-
const
|
|
214
|
+
const p10 = scanServer(name, entry).then((r) => {
|
|
213
215
|
results.push(r);
|
|
214
|
-
executing.delete(
|
|
216
|
+
executing.delete(p10);
|
|
215
217
|
});
|
|
216
|
-
executing.add(
|
|
218
|
+
executing.add(p10);
|
|
217
219
|
if (executing.size >= concurrency) await Promise.race(executing);
|
|
218
220
|
}
|
|
219
221
|
await Promise.all(executing);
|
|
220
222
|
return results;
|
|
221
223
|
}
|
|
222
224
|
|
|
225
|
+
// src/core/version-checker.ts
|
|
226
|
+
function compareVersions(a, b) {
|
|
227
|
+
const aParts = a.replace(/^v/, "").split(".").map(Number);
|
|
228
|
+
const bParts = b.replace(/^v/, "").split(".").map(Number);
|
|
229
|
+
const len = Math.max(aParts.length, bParts.length);
|
|
230
|
+
for (let i = 0; i < len; i++) {
|
|
231
|
+
const aN = aParts[i] ?? 0;
|
|
232
|
+
const bN = bParts[i] ?? 0;
|
|
233
|
+
if (Number.isNaN(aN) || Number.isNaN(bN)) return 0;
|
|
234
|
+
if (aN < bN) return -1;
|
|
235
|
+
if (aN > bN) return 1;
|
|
236
|
+
}
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
function detectUpdateType(current, latest) {
|
|
240
|
+
const cParts = current.replace(/^v/, "").split(".").map(Number);
|
|
241
|
+
const lParts = latest.replace(/^v/, "").split(".").map(Number);
|
|
242
|
+
if ((lParts[0] ?? 0) > (cParts[0] ?? 0)) return "major";
|
|
243
|
+
if ((lParts[1] ?? 0) > (cParts[1] ?? 0)) return "minor";
|
|
244
|
+
return "patch";
|
|
245
|
+
}
|
|
246
|
+
async function fetchNpmLatest(packageName) {
|
|
247
|
+
try {
|
|
248
|
+
const res = await fetch(
|
|
249
|
+
`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`,
|
|
250
|
+
{
|
|
251
|
+
headers: { Accept: "application/json" },
|
|
252
|
+
signal: AbortSignal.timeout(8e3)
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
if (!res.ok) return null;
|
|
256
|
+
const data = await res.json();
|
|
257
|
+
return typeof data.version === "string" ? data.version : null;
|
|
258
|
+
} catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async function fetchSmitheryLatest(name) {
|
|
263
|
+
try {
|
|
264
|
+
const res = await fetch(
|
|
265
|
+
`https://registry.smithery.ai/servers/${encodeURIComponent(name)}`,
|
|
266
|
+
{
|
|
267
|
+
headers: { Accept: "application/json" },
|
|
268
|
+
signal: AbortSignal.timeout(8e3)
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
if (!res.ok) return null;
|
|
272
|
+
const data = await res.json();
|
|
273
|
+
return typeof data.version === "string" ? data.version : null;
|
|
274
|
+
} catch {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function fetchGithubLatest(resolved) {
|
|
279
|
+
const match = resolved.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
280
|
+
if (!match) return null;
|
|
281
|
+
const [, owner, repo] = match;
|
|
282
|
+
try {
|
|
283
|
+
const res = await fetch(
|
|
284
|
+
`https://api.github.com/repos/${owner}/${repo}/releases/latest`,
|
|
285
|
+
{
|
|
286
|
+
headers: { Accept: "application/json" },
|
|
287
|
+
signal: AbortSignal.timeout(8e3)
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
if (!res.ok) return null;
|
|
291
|
+
const data = await res.json();
|
|
292
|
+
return typeof data.tag_name === "string" ? data.tag_name.replace(/^v/, "") : null;
|
|
293
|
+
} catch {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async function checkVersion(name, lockEntry) {
|
|
298
|
+
const current = lockEntry.version;
|
|
299
|
+
let latest = null;
|
|
300
|
+
if (lockEntry.source === "npm") {
|
|
301
|
+
latest = await fetchNpmLatest(name);
|
|
302
|
+
} else if (lockEntry.source === "smithery") {
|
|
303
|
+
latest = await fetchSmitheryLatest(name);
|
|
304
|
+
} else if (lockEntry.source === "github") {
|
|
305
|
+
latest = await fetchGithubLatest(lockEntry.resolved);
|
|
306
|
+
}
|
|
307
|
+
if (!latest || latest === current) {
|
|
308
|
+
return {
|
|
309
|
+
server: name,
|
|
310
|
+
source: lockEntry.source,
|
|
311
|
+
currentVersion: current,
|
|
312
|
+
latestVersion: latest ?? current,
|
|
313
|
+
hasUpdate: false
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const hasUpdate = compareVersions(current, latest) === -1;
|
|
317
|
+
return {
|
|
318
|
+
server: name,
|
|
319
|
+
source: lockEntry.source,
|
|
320
|
+
currentVersion: current,
|
|
321
|
+
latestVersion: latest,
|
|
322
|
+
hasUpdate,
|
|
323
|
+
updateType: hasUpdate ? detectUpdateType(current, latest) : void 0
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
async function checkAllVersions(lockfile) {
|
|
327
|
+
const entries = Object.entries(lockfile.servers);
|
|
328
|
+
if (entries.length === 0) return [];
|
|
329
|
+
const results = [];
|
|
330
|
+
const executing = /* @__PURE__ */ new Set();
|
|
331
|
+
for (const [name, entry] of entries) {
|
|
332
|
+
const p10 = checkVersion(name, entry).then((r) => {
|
|
333
|
+
results.push(r);
|
|
334
|
+
executing.delete(p10);
|
|
335
|
+
});
|
|
336
|
+
executing.add(p10);
|
|
337
|
+
if (executing.size >= 5) {
|
|
338
|
+
await Promise.race(executing);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
await Promise.all(executing);
|
|
342
|
+
return results;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// src/core/registry.ts
|
|
346
|
+
import { createHash } from "crypto";
|
|
347
|
+
function computeIntegrity(resolvedUrl) {
|
|
348
|
+
const hash = createHash("sha512").update(resolvedUrl).digest("base64");
|
|
349
|
+
return `sha512-${hash}`;
|
|
350
|
+
}
|
|
351
|
+
async function resolveFromSmithery(name) {
|
|
352
|
+
const url = `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`;
|
|
353
|
+
let data;
|
|
354
|
+
try {
|
|
355
|
+
const res = await fetch(url, {
|
|
356
|
+
headers: { Accept: "application/json" },
|
|
357
|
+
signal: AbortSignal.timeout(8e3)
|
|
358
|
+
});
|
|
359
|
+
if (res.status === 404) {
|
|
360
|
+
throw new Error(`Server '${name}' not found on Smithery registry`);
|
|
361
|
+
}
|
|
362
|
+
if (!res.ok) {
|
|
363
|
+
throw new Error(`Smithery API error: ${res.status}`);
|
|
364
|
+
}
|
|
365
|
+
data = await res.json();
|
|
366
|
+
} catch (err) {
|
|
367
|
+
if (err instanceof Error && err.message.includes("not found")) throw err;
|
|
368
|
+
throw new Error(
|
|
369
|
+
`Cannot reach Smithery registry: ${err instanceof Error ? err.message : String(err)}`
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
const version = typeof data.version === "string" ? data.version : "latest";
|
|
373
|
+
const command = typeof data.command === "string" ? data.command : "npx";
|
|
374
|
+
const args = Array.isArray(data.args) ? data.args : ["-y", `${name}@${version}`];
|
|
375
|
+
const envVars = Array.isArray(data.envVars) ? data.envVars : [];
|
|
376
|
+
const resolved = typeof data.resolved === "string" ? data.resolved : `smithery:${name}@${version}`;
|
|
377
|
+
return {
|
|
378
|
+
name,
|
|
379
|
+
version,
|
|
380
|
+
description: typeof data.description === "string" ? data.description : "",
|
|
381
|
+
runtime: "node",
|
|
382
|
+
command,
|
|
383
|
+
args,
|
|
384
|
+
envVars,
|
|
385
|
+
resolved
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
async function resolveFromNpm(packageName) {
|
|
389
|
+
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
|
|
390
|
+
let data;
|
|
391
|
+
try {
|
|
392
|
+
const res = await fetch(url, {
|
|
393
|
+
headers: { Accept: "application/json" },
|
|
394
|
+
signal: AbortSignal.timeout(8e3)
|
|
395
|
+
});
|
|
396
|
+
if (res.status === 404) {
|
|
397
|
+
throw new Error(`Package '${packageName}' not found on npm`);
|
|
398
|
+
}
|
|
399
|
+
if (!res.ok) {
|
|
400
|
+
throw new Error(`npm registry error: ${res.status}`);
|
|
401
|
+
}
|
|
402
|
+
data = await res.json();
|
|
403
|
+
} catch (err) {
|
|
404
|
+
if (err instanceof Error && err.message.includes("not found")) throw err;
|
|
405
|
+
throw new Error(
|
|
406
|
+
`Cannot reach npm registry: ${err instanceof Error ? err.message : String(err)}`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
const version = typeof data.version === "string" ? data.version : "latest";
|
|
410
|
+
const resolved = `https://registry.npmjs.org/${packageName}/-/${packageName.replace(/^@[^/]+\//, "")}-${version}.tgz`;
|
|
411
|
+
const mcpField = data.mcp && typeof data.mcp === "object" ? data.mcp : null;
|
|
412
|
+
const envVars = mcpField?.envVars ? mcpField.envVars : [];
|
|
413
|
+
return {
|
|
414
|
+
name: packageName,
|
|
415
|
+
version,
|
|
416
|
+
description: typeof data.description === "string" ? data.description : "",
|
|
417
|
+
runtime: "node",
|
|
418
|
+
command: "npx",
|
|
419
|
+
args: ["-y", `${packageName}@${version}`],
|
|
420
|
+
envVars,
|
|
421
|
+
resolved
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
async function resolveFromGitHub(githubUrl) {
|
|
425
|
+
const match = githubUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
426
|
+
if (!match) {
|
|
427
|
+
throw new Error(`Invalid GitHub URL: ${githubUrl}`);
|
|
428
|
+
}
|
|
429
|
+
const [, owner, repo] = match;
|
|
430
|
+
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/package.json`;
|
|
431
|
+
let pkgData = {};
|
|
432
|
+
try {
|
|
433
|
+
const res = await fetch(rawUrl, { signal: AbortSignal.timeout(8e3) });
|
|
434
|
+
if (res.ok) {
|
|
435
|
+
pkgData = await res.json();
|
|
436
|
+
}
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
const version = typeof pkgData.version === "string" ? pkgData.version : "main";
|
|
440
|
+
const name = typeof pkgData.name === "string" ? pkgData.name : `${owner}/${repo}`;
|
|
441
|
+
return {
|
|
442
|
+
name,
|
|
443
|
+
version,
|
|
444
|
+
description: typeof pkgData.description === "string" ? pkgData.description : "",
|
|
445
|
+
runtime: "node",
|
|
446
|
+
command: "npx",
|
|
447
|
+
args: ["-y", githubUrl],
|
|
448
|
+
envVars: [],
|
|
449
|
+
resolved: githubUrl
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/core/server-resolver.ts
|
|
454
|
+
function detectSource(input) {
|
|
455
|
+
if (input.startsWith("smithery:")) {
|
|
456
|
+
return { type: "smithery", input: input.slice(9) };
|
|
457
|
+
}
|
|
458
|
+
if (input.startsWith("https://github.com/") || input.startsWith("github.com/")) {
|
|
459
|
+
return { type: "github", input };
|
|
460
|
+
}
|
|
461
|
+
return { type: "npm", input };
|
|
462
|
+
}
|
|
463
|
+
function parseEnvFlags(envFlags) {
|
|
464
|
+
if (!envFlags) return {};
|
|
465
|
+
const flags = Array.isArray(envFlags) ? envFlags : [envFlags];
|
|
466
|
+
const result = {};
|
|
467
|
+
for (const flag of flags) {
|
|
468
|
+
const idx = flag.indexOf("=");
|
|
469
|
+
if (idx > 0) {
|
|
470
|
+
result[flag.slice(0, idx)] = flag.slice(idx + 1);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return result;
|
|
474
|
+
}
|
|
475
|
+
async function resolveServer(input) {
|
|
476
|
+
const source = detectSource(input);
|
|
477
|
+
switch (source.type) {
|
|
478
|
+
case "smithery":
|
|
479
|
+
return resolveFromSmithery(source.input);
|
|
480
|
+
case "github":
|
|
481
|
+
return resolveFromGitHub(source.input);
|
|
482
|
+
case "npm":
|
|
483
|
+
return resolveFromNpm(source.input);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/core/server-updater.ts
|
|
488
|
+
async function applyServerUpdate(serverName, lockEntry, clients) {
|
|
489
|
+
const fromVersion = lockEntry.version;
|
|
490
|
+
const input = lockEntry.source === "smithery" ? `smithery:${serverName}` : lockEntry.source === "github" ? lockEntry.resolved : serverName;
|
|
491
|
+
try {
|
|
492
|
+
const metadata = await resolveServer(input);
|
|
493
|
+
const integrity = computeIntegrity(metadata.resolved);
|
|
494
|
+
addEntry(serverName, {
|
|
495
|
+
...lockEntry,
|
|
496
|
+
version: metadata.version,
|
|
497
|
+
resolved: metadata.resolved,
|
|
498
|
+
integrity,
|
|
499
|
+
command: metadata.command,
|
|
500
|
+
args: metadata.args,
|
|
501
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
502
|
+
});
|
|
503
|
+
const targetClients = clients.filter(
|
|
504
|
+
(c) => lockEntry.clients.includes(c.type)
|
|
505
|
+
);
|
|
506
|
+
for (const client of targetClients) {
|
|
507
|
+
try {
|
|
508
|
+
await client.addServer(serverName, {
|
|
509
|
+
command: metadata.command,
|
|
510
|
+
args: metadata.args
|
|
511
|
+
});
|
|
512
|
+
} catch {
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return {
|
|
516
|
+
server: serverName,
|
|
517
|
+
success: true,
|
|
518
|
+
fromVersion,
|
|
519
|
+
toVersion: metadata.version
|
|
520
|
+
};
|
|
521
|
+
} catch (err) {
|
|
522
|
+
return {
|
|
523
|
+
server: serverName,
|
|
524
|
+
success: false,
|
|
525
|
+
fromVersion,
|
|
526
|
+
toVersion: fromVersion,
|
|
527
|
+
error: err instanceof Error ? err.message : String(err)
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
223
532
|
// src/commands/audit.ts
|
|
224
533
|
function colorRisk(level, score) {
|
|
225
534
|
const label = score !== null ? `${score}/100 (${level})` : level;
|
|
@@ -290,7 +599,12 @@ var audit_default = defineCommand({
|
|
|
290
599
|
},
|
|
291
600
|
fix: {
|
|
292
601
|
type: "boolean",
|
|
293
|
-
description: "
|
|
602
|
+
description: "Apply updates to fix vulnerable packages",
|
|
603
|
+
default: false
|
|
604
|
+
},
|
|
605
|
+
yes: {
|
|
606
|
+
type: "boolean",
|
|
607
|
+
description: "Skip confirmation prompt (use with --fix)",
|
|
294
608
|
default: false
|
|
295
609
|
}
|
|
296
610
|
},
|
|
@@ -351,17 +665,110 @@ var audit_default = defineCommand({
|
|
|
351
665
|
Summary: ${parts.join(" | ")}
|
|
352
666
|
`);
|
|
353
667
|
if (args.fix) {
|
|
354
|
-
|
|
355
|
-
if (withVulns.length > 0) {
|
|
356
|
-
console.log(pc.bold(" Fix suggestions:"));
|
|
357
|
-
for (const r of withVulns) {
|
|
358
|
-
console.log(` ${pc.cyan("\u2192")} Run ${pc.cyan(`mcpman install ${r.server}@latest`)} to update`);
|
|
359
|
-
}
|
|
360
|
-
console.log();
|
|
361
|
-
}
|
|
668
|
+
await runAuditFix(reports, lockfile.servers, args.yes);
|
|
362
669
|
}
|
|
363
670
|
}
|
|
364
671
|
});
|
|
672
|
+
async function loadClients() {
|
|
673
|
+
try {
|
|
674
|
+
const mod = await import("./client-detector-SUIJSIYM.js");
|
|
675
|
+
return mod.getInstalledClients();
|
|
676
|
+
} catch {
|
|
677
|
+
return [];
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async function runAuditFix(reports, servers, skipConfirm) {
|
|
681
|
+
const npmWithVulns = reports.filter(
|
|
682
|
+
(r) => r.vulnerabilities.length > 0 && r.source === "npm"
|
|
683
|
+
);
|
|
684
|
+
const nonNpmWithVulns = reports.filter(
|
|
685
|
+
(r) => r.vulnerabilities.length > 0 && r.source !== "npm"
|
|
686
|
+
);
|
|
687
|
+
if (nonNpmWithVulns.length > 0) {
|
|
688
|
+
console.log(pc.yellow(" Non-npm servers require manual update:"));
|
|
689
|
+
for (const r of nonNpmWithVulns) {
|
|
690
|
+
console.log(` ${pc.dim("\u2192")} ${r.server} (${r.source})`);
|
|
691
|
+
}
|
|
692
|
+
console.log();
|
|
693
|
+
}
|
|
694
|
+
if (npmWithVulns.length === 0) {
|
|
695
|
+
console.log(pc.green(" No fixable vulnerabilities found.\n"));
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
const versionSpinner = createSpinner("Checking for available updates...").start();
|
|
699
|
+
const versionChecks = await Promise.all(
|
|
700
|
+
npmWithVulns.map((r) => checkVersion(r.server, servers[r.server]))
|
|
701
|
+
);
|
|
702
|
+
versionSpinner.success({ text: "Version check complete" });
|
|
703
|
+
const updatable = versionChecks.filter((u) => u.hasUpdate);
|
|
704
|
+
if (updatable.length === 0) {
|
|
705
|
+
console.log(pc.yellow(
|
|
706
|
+
" Vulnerable servers have no newer versions available yet.\n Allow time for registry to publish fixes.\n"
|
|
707
|
+
));
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
console.log(pc.bold(`
|
|
711
|
+
${updatable.length} server(s) can be updated to fix vulnerabilities:
|
|
712
|
+
`));
|
|
713
|
+
for (const u of updatable) {
|
|
714
|
+
console.log(` ${pc.cyan("\u2192")} ${u.server} ${pc.dim(u.currentVersion)} \u2192 ${pc.green(u.latestVersion)}`);
|
|
715
|
+
}
|
|
716
|
+
console.log();
|
|
717
|
+
if (!skipConfirm) {
|
|
718
|
+
const confirmed = await p.confirm({
|
|
719
|
+
message: `Update ${updatable.length} vulnerable server(s)?`,
|
|
720
|
+
initialValue: true
|
|
721
|
+
});
|
|
722
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
723
|
+
p.outro("Cancelled.");
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const clients = await loadClients();
|
|
728
|
+
let successCount = 0;
|
|
729
|
+
const results = [];
|
|
730
|
+
for (const u of updatable) {
|
|
731
|
+
const s = createSpinner(`Updating ${u.server}...`).start();
|
|
732
|
+
const result = await applyServerUpdate(u.server, servers[u.server], clients);
|
|
733
|
+
if (result.success) {
|
|
734
|
+
s.success({ text: `${pc.green("\u2713")} ${u.server}: ${result.fromVersion} \u2192 ${result.toVersion}` });
|
|
735
|
+
successCount++;
|
|
736
|
+
} else {
|
|
737
|
+
s.error({ text: `${pc.red("\u2717")} ${u.server}: ${result.error}` });
|
|
738
|
+
}
|
|
739
|
+
results.push({
|
|
740
|
+
server: u.server,
|
|
741
|
+
from: result.fromVersion,
|
|
742
|
+
to: result.toVersion,
|
|
743
|
+
ok: result.success,
|
|
744
|
+
error: result.error
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
console.log();
|
|
748
|
+
if (successCount > 0) {
|
|
749
|
+
const updatedNames = results.filter((r) => r.ok).map((r) => r.server);
|
|
750
|
+
const freshLockfile = readLockfile();
|
|
751
|
+
const rescanSpinner = createSpinner("Re-scanning updated servers...").start();
|
|
752
|
+
const afterReports = await Promise.all(
|
|
753
|
+
updatedNames.map((name) => scanServer(name, freshLockfile.servers[name]))
|
|
754
|
+
);
|
|
755
|
+
rescanSpinner.success({ text: "Re-scan complete" });
|
|
756
|
+
console.log(pc.bold("\n Before / After:\n"));
|
|
757
|
+
for (const after of afterReports) {
|
|
758
|
+
const before = reports.find((r) => r.server === after.server);
|
|
759
|
+
const beforeVulns = before?.vulnerabilities.length ?? 0;
|
|
760
|
+
const afterVulns = after.vulnerabilities.length;
|
|
761
|
+
const improved = afterVulns < beforeVulns ? pc.green("improved") : pc.yellow("unchanged");
|
|
762
|
+
console.log(
|
|
763
|
+
` ${pc.bold(after.server)} vulns: ${beforeVulns} \u2192 ${afterVulns} [${improved}]`
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
console.log();
|
|
767
|
+
}
|
|
768
|
+
console.log(`
|
|
769
|
+
${successCount} of ${updatable.length} server(s) updated.
|
|
770
|
+
`);
|
|
771
|
+
}
|
|
365
772
|
|
|
366
773
|
// src/commands/doctor.ts
|
|
367
774
|
import { defineCommand as defineCommand2 } from "citty";
|
|
@@ -685,141 +1092,31 @@ function printServerResult(result, showFix) {
|
|
|
685
1092
|
console.log(` ${checkIcon} ${check.name}: ${check.message}`);
|
|
686
1093
|
if (showFix && !check.passed && !check.skipped && check.fix) {
|
|
687
1094
|
console.log(` ${pc2.yellow("\u2192")} Fix: ${pc2.cyan(check.fix)}`);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
console.log();
|
|
691
|
-
}
|
|
692
|
-
async function runParallel(tasks, concurrency) {
|
|
693
|
-
const results = [];
|
|
694
|
-
const executing = /* @__PURE__ */ new Set();
|
|
695
|
-
for (const task of tasks) {
|
|
696
|
-
const p8 = task().then((r) => {
|
|
697
|
-
results.push(r);
|
|
698
|
-
executing.delete(p8);
|
|
699
|
-
});
|
|
700
|
-
executing.add(p8);
|
|
701
|
-
if (executing.size >= concurrency) {
|
|
702
|
-
await Promise.race(executing);
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
await Promise.all(executing);
|
|
706
|
-
return results;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// src/commands/init.ts
|
|
710
|
-
import { defineCommand as defineCommand3 } from "citty";
|
|
711
|
-
import * as p from "@clack/prompts";
|
|
712
|
-
import path3 from "path";
|
|
713
|
-
|
|
714
|
-
// src/core/registry.ts
|
|
715
|
-
import { createHash } from "crypto";
|
|
716
|
-
function computeIntegrity(resolvedUrl) {
|
|
717
|
-
const hash = createHash("sha512").update(resolvedUrl).digest("base64");
|
|
718
|
-
return `sha512-${hash}`;
|
|
719
|
-
}
|
|
720
|
-
async function resolveFromSmithery(name) {
|
|
721
|
-
const url = `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`;
|
|
722
|
-
let data;
|
|
723
|
-
try {
|
|
724
|
-
const res = await fetch(url, {
|
|
725
|
-
headers: { Accept: "application/json" },
|
|
726
|
-
signal: AbortSignal.timeout(8e3)
|
|
727
|
-
});
|
|
728
|
-
if (res.status === 404) {
|
|
729
|
-
throw new Error(`Server '${name}' not found on Smithery registry`);
|
|
730
|
-
}
|
|
731
|
-
if (!res.ok) {
|
|
732
|
-
throw new Error(`Smithery API error: ${res.status}`);
|
|
733
|
-
}
|
|
734
|
-
data = await res.json();
|
|
735
|
-
} catch (err) {
|
|
736
|
-
if (err instanceof Error && err.message.includes("not found")) throw err;
|
|
737
|
-
throw new Error(
|
|
738
|
-
`Cannot reach Smithery registry: ${err instanceof Error ? err.message : String(err)}`
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
const version = typeof data.version === "string" ? data.version : "latest";
|
|
742
|
-
const command = typeof data.command === "string" ? data.command : "npx";
|
|
743
|
-
const args = Array.isArray(data.args) ? data.args : ["-y", `${name}@${version}`];
|
|
744
|
-
const envVars = Array.isArray(data.envVars) ? data.envVars : [];
|
|
745
|
-
const resolved = typeof data.resolved === "string" ? data.resolved : `smithery:${name}@${version}`;
|
|
746
|
-
return {
|
|
747
|
-
name,
|
|
748
|
-
version,
|
|
749
|
-
description: typeof data.description === "string" ? data.description : "",
|
|
750
|
-
runtime: "node",
|
|
751
|
-
command,
|
|
752
|
-
args,
|
|
753
|
-
envVars,
|
|
754
|
-
resolved
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
async function resolveFromNpm(packageName) {
|
|
758
|
-
const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
|
|
759
|
-
let data;
|
|
760
|
-
try {
|
|
761
|
-
const res = await fetch(url, {
|
|
762
|
-
headers: { Accept: "application/json" },
|
|
763
|
-
signal: AbortSignal.timeout(8e3)
|
|
764
|
-
});
|
|
765
|
-
if (res.status === 404) {
|
|
766
|
-
throw new Error(`Package '${packageName}' not found on npm`);
|
|
767
|
-
}
|
|
768
|
-
if (!res.ok) {
|
|
769
|
-
throw new Error(`npm registry error: ${res.status}`);
|
|
770
|
-
}
|
|
771
|
-
data = await res.json();
|
|
772
|
-
} catch (err) {
|
|
773
|
-
if (err instanceof Error && err.message.includes("not found")) throw err;
|
|
774
|
-
throw new Error(
|
|
775
|
-
`Cannot reach npm registry: ${err instanceof Error ? err.message : String(err)}`
|
|
776
|
-
);
|
|
1095
|
+
}
|
|
777
1096
|
}
|
|
778
|
-
|
|
779
|
-
const resolved = `https://registry.npmjs.org/${packageName}/-/${packageName.replace(/^@[^/]+\//, "")}-${version}.tgz`;
|
|
780
|
-
const mcpField = data.mcp && typeof data.mcp === "object" ? data.mcp : null;
|
|
781
|
-
const envVars = mcpField?.envVars ? mcpField.envVars : [];
|
|
782
|
-
return {
|
|
783
|
-
name: packageName,
|
|
784
|
-
version,
|
|
785
|
-
description: typeof data.description === "string" ? data.description : "",
|
|
786
|
-
runtime: "node",
|
|
787
|
-
command: "npx",
|
|
788
|
-
args: ["-y", `${packageName}@${version}`],
|
|
789
|
-
envVars,
|
|
790
|
-
resolved
|
|
791
|
-
};
|
|
1097
|
+
console.log();
|
|
792
1098
|
}
|
|
793
|
-
async function
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
pkgData = await res.json();
|
|
1099
|
+
async function runParallel(tasks, concurrency) {
|
|
1100
|
+
const results = [];
|
|
1101
|
+
const executing = /* @__PURE__ */ new Set();
|
|
1102
|
+
for (const task of tasks) {
|
|
1103
|
+
const p10 = task().then((r) => {
|
|
1104
|
+
results.push(r);
|
|
1105
|
+
executing.delete(p10);
|
|
1106
|
+
});
|
|
1107
|
+
executing.add(p10);
|
|
1108
|
+
if (executing.size >= concurrency) {
|
|
1109
|
+
await Promise.race(executing);
|
|
805
1110
|
}
|
|
806
|
-
} catch {
|
|
807
1111
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
return {
|
|
811
|
-
name,
|
|
812
|
-
version,
|
|
813
|
-
description: typeof pkgData.description === "string" ? pkgData.description : "",
|
|
814
|
-
runtime: "node",
|
|
815
|
-
command: "npx",
|
|
816
|
-
args: ["-y", githubUrl],
|
|
817
|
-
envVars: [],
|
|
818
|
-
resolved: githubUrl
|
|
819
|
-
};
|
|
1112
|
+
await Promise.all(executing);
|
|
1113
|
+
return results;
|
|
820
1114
|
}
|
|
821
1115
|
|
|
822
1116
|
// src/commands/init.ts
|
|
1117
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
1118
|
+
import * as p2 from "@clack/prompts";
|
|
1119
|
+
import path3 from "path";
|
|
823
1120
|
var init_default = defineCommand3({
|
|
824
1121
|
meta: {
|
|
825
1122
|
name: "init",
|
|
@@ -835,17 +1132,17 @@ var init_default = defineCommand3({
|
|
|
835
1132
|
},
|
|
836
1133
|
async run({ args }) {
|
|
837
1134
|
const nonInteractive = args.yes || !process.stdout.isTTY;
|
|
838
|
-
|
|
1135
|
+
p2.intro("mcpman init");
|
|
839
1136
|
const targetPath = path3.join(process.cwd(), LOCKFILE_NAME);
|
|
840
1137
|
const existing = findLockfile();
|
|
841
1138
|
if (existing) {
|
|
842
1139
|
if (nonInteractive) {
|
|
843
|
-
|
|
1140
|
+
p2.log.warn(`Lockfile already exists: ${existing} \u2014 overwriting (non-interactive).`);
|
|
844
1141
|
} else {
|
|
845
|
-
|
|
846
|
-
const overwrite = await
|
|
847
|
-
if (
|
|
848
|
-
|
|
1142
|
+
p2.log.warn(`Lockfile already exists: ${existing}`);
|
|
1143
|
+
const overwrite = await p2.confirm({ message: "Overwrite?" });
|
|
1144
|
+
if (p2.isCancel(overwrite) || !overwrite) {
|
|
1145
|
+
p2.outro("Cancelled.");
|
|
849
1146
|
return;
|
|
850
1147
|
}
|
|
851
1148
|
}
|
|
@@ -855,7 +1152,7 @@ var init_default = defineCommand3({
|
|
|
855
1152
|
const mod = await import("./client-detector-SUIJSIYM.js");
|
|
856
1153
|
clients = await mod.getInstalledClients();
|
|
857
1154
|
} catch {
|
|
858
|
-
|
|
1155
|
+
p2.log.warn("Could not detect AI clients \u2014 creating empty lockfile.");
|
|
859
1156
|
}
|
|
860
1157
|
const clientServers = [];
|
|
861
1158
|
for (const client of clients) {
|
|
@@ -869,26 +1166,26 @@ var init_default = defineCommand3({
|
|
|
869
1166
|
}
|
|
870
1167
|
createEmptyLockfile(targetPath);
|
|
871
1168
|
if (clientServers.length === 0) {
|
|
872
|
-
|
|
873
|
-
|
|
1169
|
+
p2.log.info("No existing servers found in any client config.");
|
|
1170
|
+
p2.outro(`Created ${LOCKFILE_NAME} \u2014 add it to version control!`);
|
|
874
1171
|
return;
|
|
875
1172
|
}
|
|
876
1173
|
let selected;
|
|
877
1174
|
if (nonInteractive) {
|
|
878
1175
|
selected = clientServers.map((cs) => cs.client.type);
|
|
879
|
-
|
|
1176
|
+
p2.log.info(`Non-interactive mode: importing all ${clientServers.length} client(s).`);
|
|
880
1177
|
} else {
|
|
881
1178
|
const options = clientServers.map((cs) => ({
|
|
882
1179
|
value: cs.client.type,
|
|
883
1180
|
label: `${cs.client.displayName} (${Object.keys(cs.servers).length} servers)`
|
|
884
1181
|
}));
|
|
885
|
-
const toImport = await
|
|
1182
|
+
const toImport = await p2.multiselect({
|
|
886
1183
|
message: "Import existing servers into lockfile?",
|
|
887
1184
|
options,
|
|
888
1185
|
required: false
|
|
889
1186
|
});
|
|
890
|
-
if (
|
|
891
|
-
|
|
1187
|
+
if (p2.isCancel(toImport)) {
|
|
1188
|
+
p2.outro(`Created empty ${LOCKFILE_NAME}`);
|
|
892
1189
|
return;
|
|
893
1190
|
}
|
|
894
1191
|
selected = toImport;
|
|
@@ -914,7 +1211,7 @@ var init_default = defineCommand3({
|
|
|
914
1211
|
importCount++;
|
|
915
1212
|
}
|
|
916
1213
|
}
|
|
917
|
-
|
|
1214
|
+
p2.outro(
|
|
918
1215
|
`Created ${LOCKFILE_NAME} with ${importCount} server(s) \u2014 commit to version control!`
|
|
919
1216
|
);
|
|
920
1217
|
}
|
|
@@ -924,44 +1221,42 @@ var init_default = defineCommand3({
|
|
|
924
1221
|
import { defineCommand as defineCommand4 } from "citty";
|
|
925
1222
|
|
|
926
1223
|
// src/core/installer.ts
|
|
927
|
-
import * as
|
|
1224
|
+
import * as p4 from "@clack/prompts";
|
|
928
1225
|
|
|
929
|
-
// src/core/
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
}
|
|
937
|
-
return { type: "npm", input };
|
|
938
|
-
}
|
|
939
|
-
function parseEnvFlags(envFlags) {
|
|
940
|
-
if (!envFlags) return {};
|
|
941
|
-
const flags = Array.isArray(envFlags) ? envFlags : [envFlags];
|
|
942
|
-
const result = {};
|
|
943
|
-
for (const flag of flags) {
|
|
944
|
-
const idx = flag.indexOf("=");
|
|
945
|
-
if (idx > 0) {
|
|
946
|
-
result[flag.slice(0, idx)] = flag.slice(idx + 1);
|
|
1226
|
+
// src/core/installer-vault-helpers.ts
|
|
1227
|
+
import * as p3 from "@clack/prompts";
|
|
1228
|
+
async function tryLoadVaultSecrets(serverName) {
|
|
1229
|
+
try {
|
|
1230
|
+
const entries = listSecrets(serverName);
|
|
1231
|
+
if (entries.length === 0 || entries[0].keys.length === 0) {
|
|
1232
|
+
return {};
|
|
947
1233
|
}
|
|
1234
|
+
const password = await getMasterPassword();
|
|
1235
|
+
return getSecretsForServer(serverName, password);
|
|
1236
|
+
} catch {
|
|
1237
|
+
return {};
|
|
948
1238
|
}
|
|
949
|
-
return result;
|
|
950
1239
|
}
|
|
951
|
-
async function
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
1240
|
+
async function offerVaultSave(serverName, newVars, yes) {
|
|
1241
|
+
if (Object.keys(newVars).length === 0) return;
|
|
1242
|
+
if (yes) return;
|
|
1243
|
+
try {
|
|
1244
|
+
const save = await p3.confirm({
|
|
1245
|
+
message: `Save ${Object.keys(newVars).length} env var(s) to encrypted vault for future installs?`
|
|
1246
|
+
});
|
|
1247
|
+
if (p3.isCancel(save) || !save) return;
|
|
1248
|
+
const password = await getMasterPassword();
|
|
1249
|
+
for (const [key, value] of Object.entries(newVars)) {
|
|
1250
|
+
setSecret(serverName, key, value, password);
|
|
1251
|
+
}
|
|
1252
|
+
p3.log.success(`Credentials saved to vault for '${serverName}'`);
|
|
1253
|
+
} catch (err) {
|
|
1254
|
+
p3.log.warn(`Could not save to vault: ${err instanceof Error ? err.message : String(err)}`);
|
|
960
1255
|
}
|
|
961
1256
|
}
|
|
962
1257
|
|
|
963
1258
|
// src/core/installer.ts
|
|
964
|
-
async function
|
|
1259
|
+
async function loadClients2() {
|
|
965
1260
|
try {
|
|
966
1261
|
const mod = await import("./client-detector-SUIJSIYM.js");
|
|
967
1262
|
return mod.getInstalledClients();
|
|
@@ -970,65 +1265,68 @@ async function loadClients() {
|
|
|
970
1265
|
}
|
|
971
1266
|
}
|
|
972
1267
|
async function installServer(input, options = {}) {
|
|
973
|
-
|
|
974
|
-
const spinner5 =
|
|
1268
|
+
p4.intro("mcpman install");
|
|
1269
|
+
const spinner5 = p4.spinner();
|
|
975
1270
|
spinner5.start("Resolving server...");
|
|
976
1271
|
let metadata;
|
|
977
1272
|
try {
|
|
978
1273
|
metadata = await resolveServer(input);
|
|
979
1274
|
} catch (err) {
|
|
980
1275
|
spinner5.stop("Resolution failed");
|
|
981
|
-
|
|
1276
|
+
p4.log.error(err instanceof Error ? err.message : String(err));
|
|
982
1277
|
process.exit(1);
|
|
983
1278
|
}
|
|
984
1279
|
spinner5.stop(`Found: ${metadata.name}@${metadata.version}`);
|
|
985
|
-
const clients = await
|
|
1280
|
+
const clients = await loadClients2();
|
|
986
1281
|
if (clients.length === 0) {
|
|
987
|
-
|
|
988
|
-
|
|
1282
|
+
p4.log.warn("No supported AI clients detected on this machine.");
|
|
1283
|
+
p4.log.info("Supported: Claude Desktop, Cursor, VS Code, Windsurf");
|
|
989
1284
|
process.exit(1);
|
|
990
1285
|
}
|
|
991
1286
|
let selectedClients;
|
|
992
1287
|
if (options.client) {
|
|
993
1288
|
const found = clients.find((c) => c.type === options.client || c.displayName.toLowerCase() === options.client?.toLowerCase());
|
|
994
1289
|
if (!found) {
|
|
995
|
-
|
|
996
|
-
|
|
1290
|
+
p4.log.error(`Client '${options.client}' not found or not installed.`);
|
|
1291
|
+
p4.log.info(`Available: ${clients.map((c) => c.type).join(", ")}`);
|
|
997
1292
|
process.exit(1);
|
|
998
1293
|
}
|
|
999
1294
|
selectedClients = [found];
|
|
1000
1295
|
} else if (options.yes || clients.length === 1) {
|
|
1001
1296
|
selectedClients = clients;
|
|
1002
1297
|
} else {
|
|
1003
|
-
const chosen = await
|
|
1298
|
+
const chosen = await p4.multiselect({
|
|
1004
1299
|
message: "Install to which client(s)?",
|
|
1005
1300
|
options: clients.map((c) => ({ value: c.type, label: c.displayName })),
|
|
1006
1301
|
required: true
|
|
1007
1302
|
});
|
|
1008
|
-
if (
|
|
1009
|
-
|
|
1303
|
+
if (p4.isCancel(chosen)) {
|
|
1304
|
+
p4.outro("Cancelled.");
|
|
1010
1305
|
process.exit(0);
|
|
1011
1306
|
}
|
|
1012
1307
|
selectedClients = clients.filter((c) => chosen.includes(c.type));
|
|
1013
1308
|
}
|
|
1014
1309
|
const providedEnv = parseEnvFlags(options.env);
|
|
1015
|
-
const
|
|
1310
|
+
const vaultEnv = await tryLoadVaultSecrets(metadata.name);
|
|
1311
|
+
const collectedEnv = { ...vaultEnv, ...providedEnv };
|
|
1312
|
+
const newlyEnteredVars = {};
|
|
1016
1313
|
const requiredVars = metadata.envVars.filter((e) => e.required && !(e.name in collectedEnv));
|
|
1017
1314
|
for (const envVar of requiredVars) {
|
|
1018
1315
|
if (options.yes && envVar.default) {
|
|
1019
1316
|
collectedEnv[envVar.name] = envVar.default;
|
|
1020
1317
|
continue;
|
|
1021
1318
|
}
|
|
1022
|
-
const val = await
|
|
1319
|
+
const val = await p4.text({
|
|
1023
1320
|
message: `${envVar.name}${envVar.description ? ` \u2014 ${envVar.description}` : ""}`,
|
|
1024
1321
|
placeholder: envVar.default ?? "",
|
|
1025
1322
|
validate: (v) => envVar.required && !v ? "Required" : void 0
|
|
1026
1323
|
});
|
|
1027
|
-
if (
|
|
1028
|
-
|
|
1324
|
+
if (p4.isCancel(val)) {
|
|
1325
|
+
p4.outro("Cancelled.");
|
|
1029
1326
|
process.exit(0);
|
|
1030
1327
|
}
|
|
1031
1328
|
collectedEnv[envVar.name] = val;
|
|
1329
|
+
newlyEnteredVars[envVar.name] = val;
|
|
1032
1330
|
}
|
|
1033
1331
|
const entry = {
|
|
1034
1332
|
command: metadata.command,
|
|
@@ -1043,7 +1341,7 @@ async function installServer(input, options = {}) {
|
|
|
1043
1341
|
clientTypes.push(client.type);
|
|
1044
1342
|
} catch (err) {
|
|
1045
1343
|
spinner5.stop("Partial failure");
|
|
1046
|
-
|
|
1344
|
+
p4.log.warn(`Failed to write to ${client.displayName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1047
1345
|
}
|
|
1048
1346
|
}
|
|
1049
1347
|
spinner5.stop("Config written");
|
|
@@ -1062,8 +1360,9 @@ async function installServer(input, options = {}) {
|
|
|
1062
1360
|
clients: clientTypes
|
|
1063
1361
|
});
|
|
1064
1362
|
const lockPath = findLockfile() ?? "mcpman.lock (global)";
|
|
1065
|
-
|
|
1066
|
-
|
|
1363
|
+
p4.log.success(`Lockfile updated: ${lockPath}`);
|
|
1364
|
+
await offerVaultSave(metadata.name, newlyEnteredVars, options.yes ?? false);
|
|
1365
|
+
p4.outro(`${metadata.name}@${metadata.version} installed to ${clientTypes.join(", ")}`);
|
|
1067
1366
|
}
|
|
1068
1367
|
|
|
1069
1368
|
// src/utils/logger.ts
|
|
@@ -1087,7 +1386,7 @@ function json(data) {
|
|
|
1087
1386
|
}
|
|
1088
1387
|
|
|
1089
1388
|
// src/commands/install.ts
|
|
1090
|
-
import * as
|
|
1389
|
+
import * as p5 from "@clack/prompts";
|
|
1091
1390
|
var install_default = defineCommand4({
|
|
1092
1391
|
meta: {
|
|
1093
1392
|
name: "install",
|
|
@@ -1137,8 +1436,8 @@ async function restoreFromLockfile() {
|
|
|
1137
1436
|
info("Lockfile is empty \u2014 nothing to restore.");
|
|
1138
1437
|
return;
|
|
1139
1438
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1439
|
+
p5.intro(`mcpman install (restore from ${lockPath})`);
|
|
1440
|
+
p5.log.info(`Restoring ${entries.length} server(s)...`);
|
|
1142
1441
|
for (const [name, entry] of entries) {
|
|
1143
1442
|
const input = entry.source === "smithery" ? `smithery:${name}` : entry.source === "github" ? entry.resolved : name;
|
|
1144
1443
|
await installServer(input, {
|
|
@@ -1146,7 +1445,7 @@ async function restoreFromLockfile() {
|
|
|
1146
1445
|
yes: true
|
|
1147
1446
|
});
|
|
1148
1447
|
}
|
|
1149
|
-
|
|
1448
|
+
p5.outro("Restore complete.");
|
|
1150
1449
|
}
|
|
1151
1450
|
|
|
1152
1451
|
// src/commands/list.ts
|
|
@@ -1232,7 +1531,7 @@ function formatClients(clients) {
|
|
|
1232
1531
|
|
|
1233
1532
|
// src/commands/remove.ts
|
|
1234
1533
|
import { defineCommand as defineCommand6 } from "citty";
|
|
1235
|
-
import * as
|
|
1534
|
+
import * as p6 from "@clack/prompts";
|
|
1236
1535
|
import pc5 from "picocolors";
|
|
1237
1536
|
var CLIENT_DISPLAY2 = {
|
|
1238
1537
|
"claude-desktop": "Claude",
|
|
@@ -1270,17 +1569,17 @@ var remove_default = defineCommand6({
|
|
|
1270
1569
|
}
|
|
1271
1570
|
},
|
|
1272
1571
|
async run({ args }) {
|
|
1273
|
-
|
|
1572
|
+
p6.intro(pc5.bold("mcpman remove"));
|
|
1274
1573
|
const serverName = args.server;
|
|
1275
1574
|
const servers = await getInstalledServers();
|
|
1276
1575
|
const match = servers.find((s) => s.name === serverName);
|
|
1277
1576
|
if (!match) {
|
|
1278
|
-
|
|
1577
|
+
p6.log.warn(`Server "${serverName}" is not installed.`);
|
|
1279
1578
|
const similar = servers.filter((s) => s.name.includes(serverName) || serverName.includes(s.name));
|
|
1280
1579
|
if (similar.length > 0) {
|
|
1281
|
-
|
|
1580
|
+
p6.log.info(`Did you mean: ${similar.map((s) => pc5.cyan(s.name)).join(", ")}?`);
|
|
1282
1581
|
}
|
|
1283
|
-
|
|
1582
|
+
p6.outro("Nothing to remove.");
|
|
1284
1583
|
return;
|
|
1285
1584
|
}
|
|
1286
1585
|
let targetClients;
|
|
@@ -1288,15 +1587,15 @@ var remove_default = defineCommand6({
|
|
|
1288
1587
|
targetClients = match.clients;
|
|
1289
1588
|
} else if (args.client) {
|
|
1290
1589
|
if (!match.clients.includes(args.client)) {
|
|
1291
|
-
|
|
1292
|
-
|
|
1590
|
+
p6.log.warn(`Server "${serverName}" is not installed in client "${args.client}".`);
|
|
1591
|
+
p6.outro("Nothing to remove.");
|
|
1293
1592
|
return;
|
|
1294
1593
|
}
|
|
1295
1594
|
targetClients = [args.client];
|
|
1296
1595
|
} else if (match.clients.length === 1) {
|
|
1297
1596
|
targetClients = match.clients;
|
|
1298
1597
|
} else {
|
|
1299
|
-
const selected = await
|
|
1598
|
+
const selected = await p6.multiselect({
|
|
1300
1599
|
message: `Remove "${serverName}" from which clients?`,
|
|
1301
1600
|
options: match.clients.map((c) => ({
|
|
1302
1601
|
value: c,
|
|
@@ -1304,19 +1603,19 @@ var remove_default = defineCommand6({
|
|
|
1304
1603
|
})),
|
|
1305
1604
|
required: true
|
|
1306
1605
|
});
|
|
1307
|
-
if (
|
|
1308
|
-
|
|
1606
|
+
if (p6.isCancel(selected)) {
|
|
1607
|
+
p6.outro("Cancelled.");
|
|
1309
1608
|
process.exit(0);
|
|
1310
1609
|
}
|
|
1311
1610
|
targetClients = selected;
|
|
1312
1611
|
}
|
|
1313
1612
|
if (!args.yes) {
|
|
1314
1613
|
const clientNames = targetClients.map(clientDisplayName).join(", ");
|
|
1315
|
-
const confirmed = await
|
|
1614
|
+
const confirmed = await p6.confirm({
|
|
1316
1615
|
message: `Remove ${pc5.cyan(serverName)} from ${pc5.yellow(clientNames)}?`
|
|
1317
1616
|
});
|
|
1318
|
-
if (
|
|
1319
|
-
|
|
1617
|
+
if (p6.isCancel(confirmed) || !confirmed) {
|
|
1618
|
+
p6.outro("Cancelled.");
|
|
1320
1619
|
return;
|
|
1321
1620
|
}
|
|
1322
1621
|
}
|
|
@@ -1330,25 +1629,25 @@ var remove_default = defineCommand6({
|
|
|
1330
1629
|
}
|
|
1331
1630
|
try {
|
|
1332
1631
|
await handler.removeServer(serverName);
|
|
1333
|
-
|
|
1632
|
+
p6.log.success(`Removed from ${clientDisplayName(clientType)}`);
|
|
1334
1633
|
} catch (err) {
|
|
1335
1634
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1336
1635
|
errors.push(`${clientDisplayName(clientType)}: ${msg}`);
|
|
1337
1636
|
}
|
|
1338
1637
|
}
|
|
1339
1638
|
if (errors.length > 0) {
|
|
1340
|
-
for (const e of errors)
|
|
1341
|
-
|
|
1639
|
+
for (const e of errors) p6.log.error(e);
|
|
1640
|
+
p6.outro(pc5.red("Completed with errors."));
|
|
1342
1641
|
process.exit(1);
|
|
1343
1642
|
}
|
|
1344
|
-
|
|
1643
|
+
p6.outro(pc5.green(`Removed "${serverName}" successfully.`));
|
|
1345
1644
|
}
|
|
1346
1645
|
});
|
|
1347
1646
|
|
|
1348
1647
|
// src/commands/secrets.ts
|
|
1349
1648
|
import { defineCommand as defineCommand7 } from "citty";
|
|
1350
1649
|
import pc6 from "picocolors";
|
|
1351
|
-
import * as
|
|
1650
|
+
import * as p7 from "@clack/prompts";
|
|
1352
1651
|
function maskValue(value) {
|
|
1353
1652
|
if (value.length <= 8) return "***";
|
|
1354
1653
|
return `${value.slice(0, 4)}***${value.slice(-3)}`;
|
|
@@ -1378,12 +1677,12 @@ var setCommand = defineCommand7({
|
|
|
1378
1677
|
console.error(pc6.red("\u2717") + " Invalid format. Expected KEY=VALUE");
|
|
1379
1678
|
process.exit(1);
|
|
1380
1679
|
}
|
|
1381
|
-
|
|
1680
|
+
p7.intro(pc6.cyan("mcpman secrets set"));
|
|
1382
1681
|
const isNew = listSecrets(args.server).length === 0 || !listSecrets(args.server)[0]?.keys.includes(parsed.key);
|
|
1383
1682
|
const vaultPath = (await import("./vault-service-UTZAV6N6.js")).getVaultPath();
|
|
1384
1683
|
const vaultExists = (await import("fs")).existsSync(vaultPath);
|
|
1385
1684
|
const password = await getMasterPassword(!vaultExists && isNew);
|
|
1386
|
-
const spin =
|
|
1685
|
+
const spin = p7.spinner();
|
|
1387
1686
|
spin.start("Encrypting secret...");
|
|
1388
1687
|
try {
|
|
1389
1688
|
setSecret(args.server, parsed.key, parsed.value, password);
|
|
@@ -1395,7 +1694,7 @@ var setCommand = defineCommand7({
|
|
|
1395
1694
|
console.error(pc6.dim(String(err)));
|
|
1396
1695
|
process.exit(1);
|
|
1397
1696
|
}
|
|
1398
|
-
|
|
1697
|
+
p7.outro(pc6.dim("Secret encrypted and saved to vault."));
|
|
1399
1698
|
}
|
|
1400
1699
|
});
|
|
1401
1700
|
var listCommand = defineCommand7({
|
|
@@ -1441,12 +1740,12 @@ var removeCommand = defineCommand7({
|
|
|
1441
1740
|
}
|
|
1442
1741
|
},
|
|
1443
1742
|
async run({ args }) {
|
|
1444
|
-
const confirmed = await
|
|
1743
|
+
const confirmed = await p7.confirm({
|
|
1445
1744
|
message: `Remove ${pc6.bold(args.key)} from ${pc6.cyan(args.server)}?`,
|
|
1446
1745
|
initialValue: false
|
|
1447
1746
|
});
|
|
1448
|
-
if (
|
|
1449
|
-
|
|
1747
|
+
if (p7.isCancel(confirmed) || !confirmed) {
|
|
1748
|
+
p7.cancel("Cancelled.");
|
|
1450
1749
|
return;
|
|
1451
1750
|
}
|
|
1452
1751
|
try {
|
|
@@ -1473,7 +1772,7 @@ var secrets_default = defineCommand7({
|
|
|
1473
1772
|
|
|
1474
1773
|
// src/commands/sync.ts
|
|
1475
1774
|
import { defineCommand as defineCommand8 } from "citty";
|
|
1476
|
-
import * as
|
|
1775
|
+
import * as p8 from "@clack/prompts";
|
|
1477
1776
|
import pc7 from "picocolors";
|
|
1478
1777
|
|
|
1479
1778
|
// src/core/config-diff.ts
|
|
@@ -1489,7 +1788,7 @@ function reconstructServerEntry(lockEntry) {
|
|
|
1489
1788
|
}
|
|
1490
1789
|
return entry;
|
|
1491
1790
|
}
|
|
1492
|
-
function computeDiff(lockfile, clientConfigs) {
|
|
1791
|
+
function computeDiff(lockfile, clientConfigs, options = {}) {
|
|
1493
1792
|
const actions = [];
|
|
1494
1793
|
for (const [server, lockEntry] of Object.entries(lockfile.servers)) {
|
|
1495
1794
|
for (const client of lockEntry.clients) {
|
|
@@ -1507,19 +1806,21 @@ function computeDiff(lockfile, clientConfigs) {
|
|
|
1507
1806
|
}
|
|
1508
1807
|
}
|
|
1509
1808
|
}
|
|
1809
|
+
const extraAction = options.remove ? "remove" : "extra";
|
|
1510
1810
|
for (const [client, config] of clientConfigs) {
|
|
1511
1811
|
for (const server of Object.keys(config.servers)) {
|
|
1512
1812
|
if (!(server in lockfile.servers)) {
|
|
1513
|
-
actions.push({ server, client, action:
|
|
1813
|
+
actions.push({ server, client, action: extraAction });
|
|
1514
1814
|
}
|
|
1515
1815
|
}
|
|
1516
1816
|
}
|
|
1517
1817
|
return actions;
|
|
1518
1818
|
}
|
|
1519
|
-
function computeDiffFromClient(sourceClient, clientConfigs) {
|
|
1819
|
+
function computeDiffFromClient(sourceClient, clientConfigs, options = {}) {
|
|
1520
1820
|
const actions = [];
|
|
1521
1821
|
const sourceConfig = clientConfigs.get(sourceClient);
|
|
1522
1822
|
if (!sourceConfig) return [];
|
|
1823
|
+
const extraAction = options.remove ? "remove" : "extra";
|
|
1523
1824
|
for (const [client, config] of clientConfigs) {
|
|
1524
1825
|
if (client === sourceClient) continue;
|
|
1525
1826
|
for (const [server, entry] of Object.entries(sourceConfig.servers)) {
|
|
@@ -1531,7 +1832,7 @@ function computeDiffFromClient(sourceClient, clientConfigs) {
|
|
|
1531
1832
|
}
|
|
1532
1833
|
for (const server of Object.keys(config.servers)) {
|
|
1533
1834
|
if (!(server in sourceConfig.servers)) {
|
|
1534
|
-
actions.push({ server, client, action:
|
|
1835
|
+
actions.push({ server, client, action: extraAction });
|
|
1535
1836
|
}
|
|
1536
1837
|
}
|
|
1537
1838
|
}
|
|
@@ -1540,7 +1841,7 @@ function computeDiffFromClient(sourceClient, clientConfigs) {
|
|
|
1540
1841
|
|
|
1541
1842
|
// src/core/sync-engine.ts
|
|
1542
1843
|
async function applySyncActions(actions, clients) {
|
|
1543
|
-
const result = { applied: 0, failed: 0, errors: [] };
|
|
1844
|
+
const result = { applied: 0, removed: 0, failed: 0, errors: [] };
|
|
1544
1845
|
const addActions = actions.filter((a) => a.action === "add" && a.entry);
|
|
1545
1846
|
for (const action of addActions) {
|
|
1546
1847
|
const handler = clients.get(action.client);
|
|
@@ -1565,6 +1866,30 @@ async function applySyncActions(actions, clients) {
|
|
|
1565
1866
|
});
|
|
1566
1867
|
}
|
|
1567
1868
|
}
|
|
1869
|
+
const removeActions = actions.filter((a) => a.action === "remove");
|
|
1870
|
+
for (const action of removeActions) {
|
|
1871
|
+
const handler = clients.get(action.client);
|
|
1872
|
+
if (!handler) {
|
|
1873
|
+
result.failed++;
|
|
1874
|
+
result.errors.push({
|
|
1875
|
+
server: action.server,
|
|
1876
|
+
client: action.client,
|
|
1877
|
+
error: "No handler available for client"
|
|
1878
|
+
});
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
try {
|
|
1882
|
+
await handler.removeServer(action.server);
|
|
1883
|
+
result.removed++;
|
|
1884
|
+
} catch (err) {
|
|
1885
|
+
result.failed++;
|
|
1886
|
+
result.errors.push({
|
|
1887
|
+
server: action.server,
|
|
1888
|
+
client: action.client,
|
|
1889
|
+
error: String(err)
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1568
1893
|
return result;
|
|
1569
1894
|
}
|
|
1570
1895
|
async function getClientConfigs() {
|
|
@@ -1609,6 +1934,11 @@ var sync_default = defineCommand8({
|
|
|
1609
1934
|
description: "Preview changes without applying them",
|
|
1610
1935
|
default: false
|
|
1611
1936
|
},
|
|
1937
|
+
remove: {
|
|
1938
|
+
type: "boolean",
|
|
1939
|
+
description: "Remove extra servers not in lockfile",
|
|
1940
|
+
default: false
|
|
1941
|
+
},
|
|
1612
1942
|
source: {
|
|
1613
1943
|
type: "string",
|
|
1614
1944
|
description: "Use a specific client as source of truth (claude-desktop, cursor, vscode, windsurf)"
|
|
@@ -1620,58 +1950,64 @@ var sync_default = defineCommand8({
|
|
|
1620
1950
|
}
|
|
1621
1951
|
},
|
|
1622
1952
|
async run({ args }) {
|
|
1623
|
-
|
|
1953
|
+
p8.intro(`${pc7.cyan("mcpman sync")}`);
|
|
1624
1954
|
const sourceClient = args.source;
|
|
1625
1955
|
if (sourceClient && !VALID_CLIENTS.includes(sourceClient)) {
|
|
1626
|
-
|
|
1956
|
+
p8.log.error(`Invalid --source "${sourceClient}". Must be one of: ${VALID_CLIENTS.join(", ")}`);
|
|
1627
1957
|
process.exit(1);
|
|
1628
1958
|
}
|
|
1629
|
-
const spinner5 =
|
|
1959
|
+
const spinner5 = p8.spinner();
|
|
1630
1960
|
spinner5.start("Detecting clients and reading configs...");
|
|
1631
1961
|
const { configs, handlers } = await getClientConfigs();
|
|
1632
1962
|
spinner5.stop(`Found ${configs.size} client(s)`);
|
|
1633
1963
|
if (configs.size === 0) {
|
|
1634
|
-
|
|
1964
|
+
p8.log.warn("No AI clients detected. Install Claude Desktop, Cursor, VS Code, or Windsurf first.");
|
|
1635
1965
|
process.exit(0);
|
|
1636
1966
|
}
|
|
1967
|
+
const diffOptions = { remove: args.remove };
|
|
1637
1968
|
let actions;
|
|
1638
1969
|
if (sourceClient) {
|
|
1639
1970
|
if (!configs.has(sourceClient)) {
|
|
1640
|
-
|
|
1971
|
+
p8.log.error(`Source client "${sourceClient}" is not detected or its config is unreadable.`);
|
|
1641
1972
|
process.exit(1);
|
|
1642
1973
|
}
|
|
1643
|
-
|
|
1644
|
-
actions = computeDiffFromClient(sourceClient, configs);
|
|
1974
|
+
p8.log.info(`Using ${CLIENT_DISPLAY3[sourceClient]} as source of truth`);
|
|
1975
|
+
actions = computeDiffFromClient(sourceClient, configs, diffOptions);
|
|
1645
1976
|
} else {
|
|
1646
1977
|
const lockfile = readLockfile();
|
|
1647
|
-
actions = computeDiff(lockfile, configs);
|
|
1978
|
+
actions = computeDiff(lockfile, configs, diffOptions);
|
|
1648
1979
|
}
|
|
1649
1980
|
printDiffTable(actions);
|
|
1650
1981
|
const addCount = actions.filter((a) => a.action === "add").length;
|
|
1651
1982
|
const extraCount = actions.filter((a) => a.action === "extra").length;
|
|
1652
|
-
|
|
1653
|
-
|
|
1983
|
+
const removeCount = actions.filter((a) => a.action === "remove").length;
|
|
1984
|
+
if (addCount === 0 && removeCount === 0 && extraCount === 0) {
|
|
1985
|
+
p8.outro(pc7.green("All clients are in sync."));
|
|
1654
1986
|
process.exit(0);
|
|
1655
1987
|
}
|
|
1656
1988
|
const parts = [];
|
|
1657
1989
|
if (addCount > 0) parts.push(pc7.green(`${addCount} to add`));
|
|
1990
|
+
if (removeCount > 0) parts.push(pc7.red(`${removeCount} to remove`));
|
|
1658
1991
|
if (extraCount > 0) parts.push(pc7.yellow(`${extraCount} extra (informational)`));
|
|
1659
|
-
|
|
1992
|
+
p8.log.info(parts.join(" \xB7 "));
|
|
1660
1993
|
if (args["dry-run"]) {
|
|
1661
|
-
|
|
1994
|
+
p8.outro(pc7.dim("Dry run \u2014 no changes applied."));
|
|
1662
1995
|
process.exit(1);
|
|
1663
1996
|
}
|
|
1664
|
-
if (addCount === 0) {
|
|
1665
|
-
|
|
1997
|
+
if (addCount === 0 && removeCount === 0) {
|
|
1998
|
+
p8.outro(pc7.dim("No additions needed. Extra servers left untouched."));
|
|
1666
1999
|
process.exit(1);
|
|
1667
2000
|
}
|
|
1668
2001
|
if (!args.yes) {
|
|
1669
|
-
const
|
|
1670
|
-
|
|
2002
|
+
const actionParts = [];
|
|
2003
|
+
if (addCount > 0) actionParts.push(`${addCount} addition(s)`);
|
|
2004
|
+
if (removeCount > 0) actionParts.push(`${removeCount} removal(s)`);
|
|
2005
|
+
const confirmed = await p8.confirm({
|
|
2006
|
+
message: `Apply ${actionParts.join(" and ")} to client configs?`,
|
|
1671
2007
|
initialValue: true
|
|
1672
2008
|
});
|
|
1673
|
-
if (
|
|
1674
|
-
|
|
2009
|
+
if (p8.isCancel(confirmed) || !confirmed) {
|
|
2010
|
+
p8.outro(pc7.dim("Cancelled \u2014 no changes applied."));
|
|
1675
2011
|
process.exit(0);
|
|
1676
2012
|
}
|
|
1677
2013
|
}
|
|
@@ -1679,20 +2015,23 @@ var sync_default = defineCommand8({
|
|
|
1679
2015
|
const result = await applySyncActions(actions, handlers);
|
|
1680
2016
|
spinner5.stop("Done");
|
|
1681
2017
|
if (result.applied > 0) {
|
|
1682
|
-
|
|
2018
|
+
p8.log.success(`Added ${result.applied} server(s) to client configs.`);
|
|
2019
|
+
}
|
|
2020
|
+
if (result.removed > 0) {
|
|
2021
|
+
p8.log.success(`Removed ${result.removed} server(s) from client configs.`);
|
|
1683
2022
|
}
|
|
1684
2023
|
if (result.failed > 0) {
|
|
1685
2024
|
for (const e of result.errors) {
|
|
1686
|
-
|
|
2025
|
+
p8.log.error(`Failed to sync "${e.server}" on ${e.client}: ${e.error}`);
|
|
1687
2026
|
}
|
|
1688
2027
|
}
|
|
1689
|
-
|
|
2028
|
+
p8.outro(result.failed === 0 ? pc7.green("Sync complete.") : pc7.yellow("Sync complete with errors."));
|
|
1690
2029
|
process.exit(result.failed > 0 ? 1 : 0);
|
|
1691
2030
|
}
|
|
1692
2031
|
});
|
|
1693
2032
|
function printDiffTable(actions) {
|
|
1694
2033
|
if (actions.length === 0) {
|
|
1695
|
-
|
|
2034
|
+
p8.log.info("No actions to display.");
|
|
1696
2035
|
return;
|
|
1697
2036
|
}
|
|
1698
2037
|
const nameWidth = Math.max(6, ...actions.map((a) => a.server.length));
|
|
@@ -1713,6 +2052,8 @@ function formatAction(action) {
|
|
|
1713
2052
|
return [pc7.green("+"), pc7.green("missing \u2014 will add")];
|
|
1714
2053
|
case "extra":
|
|
1715
2054
|
return [pc7.yellow("?"), pc7.yellow("extra (not in lockfile)")];
|
|
2055
|
+
case "remove":
|
|
2056
|
+
return [pc7.red("\u2013"), pc7.red("extra \u2014 will remove")];
|
|
1716
2057
|
case "ok":
|
|
1717
2058
|
return [pc7.dim("\xB7"), pc7.dim("in sync")];
|
|
1718
2059
|
}
|
|
@@ -1723,129 +2064,9 @@ function pad2(s, width) {
|
|
|
1723
2064
|
|
|
1724
2065
|
// src/commands/update.ts
|
|
1725
2066
|
import { defineCommand as defineCommand9 } from "citty";
|
|
1726
|
-
import * as
|
|
2067
|
+
import * as p9 from "@clack/prompts";
|
|
1727
2068
|
import pc9 from "picocolors";
|
|
1728
2069
|
|
|
1729
|
-
// src/core/version-checker.ts
|
|
1730
|
-
function compareVersions(a, b) {
|
|
1731
|
-
const aParts = a.replace(/^v/, "").split(".").map(Number);
|
|
1732
|
-
const bParts = b.replace(/^v/, "").split(".").map(Number);
|
|
1733
|
-
const len = Math.max(aParts.length, bParts.length);
|
|
1734
|
-
for (let i = 0; i < len; i++) {
|
|
1735
|
-
const aN = aParts[i] ?? 0;
|
|
1736
|
-
const bN = bParts[i] ?? 0;
|
|
1737
|
-
if (Number.isNaN(aN) || Number.isNaN(bN)) return 0;
|
|
1738
|
-
if (aN < bN) return -1;
|
|
1739
|
-
if (aN > bN) return 1;
|
|
1740
|
-
}
|
|
1741
|
-
return 0;
|
|
1742
|
-
}
|
|
1743
|
-
function detectUpdateType(current, latest) {
|
|
1744
|
-
const cParts = current.replace(/^v/, "").split(".").map(Number);
|
|
1745
|
-
const lParts = latest.replace(/^v/, "").split(".").map(Number);
|
|
1746
|
-
if ((lParts[0] ?? 0) > (cParts[0] ?? 0)) return "major";
|
|
1747
|
-
if ((lParts[1] ?? 0) > (cParts[1] ?? 0)) return "minor";
|
|
1748
|
-
return "patch";
|
|
1749
|
-
}
|
|
1750
|
-
async function fetchNpmLatest(packageName) {
|
|
1751
|
-
try {
|
|
1752
|
-
const res = await fetch(
|
|
1753
|
-
`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`,
|
|
1754
|
-
{
|
|
1755
|
-
headers: { Accept: "application/json" },
|
|
1756
|
-
signal: AbortSignal.timeout(8e3)
|
|
1757
|
-
}
|
|
1758
|
-
);
|
|
1759
|
-
if (!res.ok) return null;
|
|
1760
|
-
const data = await res.json();
|
|
1761
|
-
return typeof data.version === "string" ? data.version : null;
|
|
1762
|
-
} catch {
|
|
1763
|
-
return null;
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
async function fetchSmitheryLatest(name) {
|
|
1767
|
-
try {
|
|
1768
|
-
const res = await fetch(
|
|
1769
|
-
`https://registry.smithery.ai/servers/${encodeURIComponent(name)}`,
|
|
1770
|
-
{
|
|
1771
|
-
headers: { Accept: "application/json" },
|
|
1772
|
-
signal: AbortSignal.timeout(8e3)
|
|
1773
|
-
}
|
|
1774
|
-
);
|
|
1775
|
-
if (!res.ok) return null;
|
|
1776
|
-
const data = await res.json();
|
|
1777
|
-
return typeof data.version === "string" ? data.version : null;
|
|
1778
|
-
} catch {
|
|
1779
|
-
return null;
|
|
1780
|
-
}
|
|
1781
|
-
}
|
|
1782
|
-
async function fetchGithubLatest(resolved) {
|
|
1783
|
-
const match = resolved.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
1784
|
-
if (!match) return null;
|
|
1785
|
-
const [, owner, repo] = match;
|
|
1786
|
-
try {
|
|
1787
|
-
const res = await fetch(
|
|
1788
|
-
`https://api.github.com/repos/${owner}/${repo}/releases/latest`,
|
|
1789
|
-
{
|
|
1790
|
-
headers: { Accept: "application/json" },
|
|
1791
|
-
signal: AbortSignal.timeout(8e3)
|
|
1792
|
-
}
|
|
1793
|
-
);
|
|
1794
|
-
if (!res.ok) return null;
|
|
1795
|
-
const data = await res.json();
|
|
1796
|
-
return typeof data.tag_name === "string" ? data.tag_name.replace(/^v/, "") : null;
|
|
1797
|
-
} catch {
|
|
1798
|
-
return null;
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
async function checkVersion(name, lockEntry) {
|
|
1802
|
-
const current = lockEntry.version;
|
|
1803
|
-
let latest = null;
|
|
1804
|
-
if (lockEntry.source === "npm") {
|
|
1805
|
-
latest = await fetchNpmLatest(name);
|
|
1806
|
-
} else if (lockEntry.source === "smithery") {
|
|
1807
|
-
latest = await fetchSmitheryLatest(name);
|
|
1808
|
-
} else if (lockEntry.source === "github") {
|
|
1809
|
-
latest = await fetchGithubLatest(lockEntry.resolved);
|
|
1810
|
-
}
|
|
1811
|
-
if (!latest || latest === current) {
|
|
1812
|
-
return {
|
|
1813
|
-
server: name,
|
|
1814
|
-
source: lockEntry.source,
|
|
1815
|
-
currentVersion: current,
|
|
1816
|
-
latestVersion: latest ?? current,
|
|
1817
|
-
hasUpdate: false
|
|
1818
|
-
};
|
|
1819
|
-
}
|
|
1820
|
-
const hasUpdate = compareVersions(current, latest) === -1;
|
|
1821
|
-
return {
|
|
1822
|
-
server: name,
|
|
1823
|
-
source: lockEntry.source,
|
|
1824
|
-
currentVersion: current,
|
|
1825
|
-
latestVersion: latest,
|
|
1826
|
-
hasUpdate,
|
|
1827
|
-
updateType: hasUpdate ? detectUpdateType(current, latest) : void 0
|
|
1828
|
-
};
|
|
1829
|
-
}
|
|
1830
|
-
async function checkAllVersions(lockfile) {
|
|
1831
|
-
const entries = Object.entries(lockfile.servers);
|
|
1832
|
-
if (entries.length === 0) return [];
|
|
1833
|
-
const results = [];
|
|
1834
|
-
const executing = /* @__PURE__ */ new Set();
|
|
1835
|
-
for (const [name, entry] of entries) {
|
|
1836
|
-
const p8 = checkVersion(name, entry).then((r) => {
|
|
1837
|
-
results.push(r);
|
|
1838
|
-
executing.delete(p8);
|
|
1839
|
-
});
|
|
1840
|
-
executing.add(p8);
|
|
1841
|
-
if (executing.size >= 5) {
|
|
1842
|
-
await Promise.race(executing);
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
await Promise.all(executing);
|
|
1846
|
-
return results;
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
2070
|
// src/core/update-notifier.ts
|
|
1850
2071
|
import fs3 from "fs";
|
|
1851
2072
|
import path4 from "path";
|
|
@@ -1865,7 +2086,7 @@ function writeUpdateCache(data) {
|
|
|
1865
2086
|
}
|
|
1866
2087
|
|
|
1867
2088
|
// src/commands/update.ts
|
|
1868
|
-
async function
|
|
2089
|
+
async function loadClients3() {
|
|
1869
2090
|
try {
|
|
1870
2091
|
const mod = await import("./client-detector-SUIJSIYM.js");
|
|
1871
2092
|
return mod.getInstalledClients();
|
|
@@ -1933,7 +2154,7 @@ var update_default = defineCommand9({
|
|
|
1933
2154
|
}
|
|
1934
2155
|
process.exit(1);
|
|
1935
2156
|
}
|
|
1936
|
-
const spinner5 =
|
|
2157
|
+
const spinner5 = p9.spinner();
|
|
1937
2158
|
spinner5.start("Checking versions...");
|
|
1938
2159
|
let updates;
|
|
1939
2160
|
try {
|
|
@@ -1963,62 +2184,42 @@ var update_default = defineCommand9({
|
|
|
1963
2184
|
return;
|
|
1964
2185
|
}
|
|
1965
2186
|
if (!args.yes) {
|
|
1966
|
-
const confirmed = await
|
|
2187
|
+
const confirmed = await p9.confirm({
|
|
1967
2188
|
message: `Apply ${outdated.length} update(s)?`,
|
|
1968
2189
|
initialValue: true
|
|
1969
2190
|
});
|
|
1970
|
-
if (
|
|
1971
|
-
|
|
2191
|
+
if (p9.isCancel(confirmed) || !confirmed) {
|
|
2192
|
+
p9.outro("Cancelled.");
|
|
1972
2193
|
return;
|
|
1973
2194
|
}
|
|
1974
2195
|
}
|
|
1975
|
-
const clients = await
|
|
2196
|
+
const clients = await loadClients3();
|
|
1976
2197
|
let successCount = 0;
|
|
1977
2198
|
for (const update of outdated) {
|
|
1978
|
-
const
|
|
1979
|
-
const input = lockEntry.source === "smithery" ? `smithery:${update.server}` : lockEntry.source === "github" ? lockEntry.resolved : update.server;
|
|
1980
|
-
const s = p7.spinner();
|
|
2199
|
+
const s = p9.spinner();
|
|
1981
2200
|
s.start(`Updating ${update.server}...`);
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
integrity,
|
|
1990
|
-
command: metadata.command,
|
|
1991
|
-
args: metadata.args,
|
|
1992
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1993
|
-
});
|
|
1994
|
-
const entryClients = clients.filter(
|
|
1995
|
-
(c) => lockEntry.clients.includes(c.type)
|
|
1996
|
-
);
|
|
1997
|
-
for (const client of entryClients) {
|
|
1998
|
-
try {
|
|
1999
|
-
await client.addServer(update.server, {
|
|
2000
|
-
command: metadata.command,
|
|
2001
|
-
args: metadata.args
|
|
2002
|
-
});
|
|
2003
|
-
} catch {
|
|
2004
|
-
}
|
|
2005
|
-
}
|
|
2006
|
-
s.stop(`${pc9.green("\u2713")} ${update.server}: ${update.currentVersion} \u2192 ${metadata.version}`);
|
|
2201
|
+
const result = await applyServerUpdate(
|
|
2202
|
+
update.server,
|
|
2203
|
+
servers[update.server],
|
|
2204
|
+
clients
|
|
2205
|
+
);
|
|
2206
|
+
if (result.success) {
|
|
2207
|
+
s.stop(`${pc9.green("\u2713")} ${update.server}: ${result.fromVersion} \u2192 ${result.toVersion}`);
|
|
2007
2208
|
successCount++;
|
|
2008
|
-
}
|
|
2009
|
-
s.stop(`${pc9.red("\u2717")} ${update.server}: ${
|
|
2209
|
+
} else {
|
|
2210
|
+
s.stop(`${pc9.red("\u2717")} ${update.server}: ${result.error}`);
|
|
2010
2211
|
}
|
|
2011
2212
|
}
|
|
2012
2213
|
const freshLockfile = readLockfile(resolveLockfilePath());
|
|
2013
2214
|
const freshUpdates = await checkAllVersions(freshLockfile);
|
|
2014
2215
|
writeUpdateCache({ lastCheck: (/* @__PURE__ */ new Date()).toISOString(), updates: freshUpdates });
|
|
2015
|
-
|
|
2216
|
+
p9.outro(`${successCount} of ${outdated.length} server(s) updated.`);
|
|
2016
2217
|
}
|
|
2017
2218
|
});
|
|
2018
2219
|
|
|
2019
2220
|
// src/utils/constants.ts
|
|
2020
2221
|
var APP_NAME = "mcpman";
|
|
2021
|
-
var APP_VERSION = "0.
|
|
2222
|
+
var APP_VERSION = "0.3.0";
|
|
2022
2223
|
var APP_DESCRIPTION = "The package manager for MCP servers";
|
|
2023
2224
|
|
|
2024
2225
|
// src/index.ts
|