mcpman 0.1.1 → 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/dist/index.js CHANGED
@@ -2,13 +2,777 @@
2
2
  import {
3
3
  getInstalledClients
4
4
  } from "./chunk-QY22QTBR.js";
5
+ import {
6
+ getMasterPassword,
7
+ getSecretsForServer,
8
+ listSecrets,
9
+ removeSecret,
10
+ setSecret
11
+ } from "./chunk-6X6Q6UZC.js";
5
12
 
6
13
  // src/index.ts
7
- import { defineCommand as defineCommand6, runMain } from "citty";
14
+ import { defineCommand as defineCommand10, runMain } from "citty";
15
+
16
+ // src/commands/audit.ts
17
+ import { defineCommand } from "citty";
18
+ import * as p from "@clack/prompts";
19
+ import pc from "picocolors";
20
+ import { createSpinner } from "nanospinner";
21
+
22
+ // src/core/lockfile.ts
23
+ import fs from "fs";
24
+ import path from "path";
25
+ import os from "os";
26
+ var LOCKFILE_NAME = "mcpman.lock";
27
+ function findLockfile() {
28
+ let dir = process.cwd();
29
+ while (true) {
30
+ const candidate = path.join(dir, LOCKFILE_NAME);
31
+ if (fs.existsSync(candidate)) return candidate;
32
+ const parent = path.dirname(dir);
33
+ if (parent === dir) break;
34
+ dir = parent;
35
+ }
36
+ return null;
37
+ }
38
+ function getGlobalLockfilePath() {
39
+ return path.join(os.homedir(), ".mcpman", LOCKFILE_NAME);
40
+ }
41
+ function resolveLockfilePath() {
42
+ return findLockfile() ?? getGlobalLockfilePath();
43
+ }
44
+ function readLockfile(filePath) {
45
+ const target = filePath ?? resolveLockfilePath();
46
+ if (!fs.existsSync(target)) {
47
+ return { lockfileVersion: 1, servers: {} };
48
+ }
49
+ try {
50
+ const raw = fs.readFileSync(target, "utf-8");
51
+ return JSON.parse(raw);
52
+ } catch {
53
+ return { lockfileVersion: 1, servers: {} };
54
+ }
55
+ }
56
+ function serialize(data) {
57
+ const sorted = {
58
+ lockfileVersion: data.lockfileVersion,
59
+ servers: Object.fromEntries(
60
+ Object.entries(data.servers).sort(([a], [b]) => a.localeCompare(b))
61
+ )
62
+ };
63
+ return JSON.stringify(sorted, null, 2) + "\n";
64
+ }
65
+ function writeLockfile(data, filePath) {
66
+ const target = filePath ?? resolveLockfilePath();
67
+ const dir = path.dirname(target);
68
+ if (!fs.existsSync(dir)) {
69
+ fs.mkdirSync(dir, { recursive: true });
70
+ }
71
+ const tmp = `${target}.tmp`;
72
+ fs.writeFileSync(tmp, serialize(data), "utf-8");
73
+ fs.renameSync(tmp, target);
74
+ }
75
+ function addEntry(name, entry, filePath) {
76
+ const data = readLockfile(filePath);
77
+ data.servers[name] = entry;
78
+ writeLockfile(data, filePath);
79
+ }
80
+ function createEmptyLockfile(filePath) {
81
+ writeLockfile({ lockfileVersion: 1, servers: {} }, filePath);
82
+ }
83
+
84
+ // src/core/security-scanner.ts
85
+ import fs2 from "fs";
86
+ import os2 from "os";
87
+ import path2 from "path";
88
+ var CACHE_PATH = path2.join(os2.homedir(), ".mcpman", ".audit-cache.json");
89
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
90
+ function readCache() {
91
+ try {
92
+ if (!fs2.existsSync(CACHE_PATH)) return {};
93
+ return JSON.parse(fs2.readFileSync(CACHE_PATH, "utf-8"));
94
+ } catch {
95
+ return {};
96
+ }
97
+ }
98
+ function writeCache(cache) {
99
+ const dir = path2.dirname(CACHE_PATH);
100
+ if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
101
+ const tmp = `${CACHE_PATH}.tmp`;
102
+ fs2.writeFileSync(tmp, JSON.stringify(cache, null, 2), "utf-8");
103
+ fs2.renameSync(tmp, CACHE_PATH);
104
+ }
105
+ function getCachedReport(name, version) {
106
+ const cache = readCache();
107
+ const key = `${name}@${version}`;
108
+ const entry = cache[key];
109
+ if (!entry) return null;
110
+ if (Date.now() - entry.timestamp > CACHE_TTL_MS) return null;
111
+ return entry.report;
112
+ }
113
+ function cacheReport(name, version, report) {
114
+ const cache = readCache();
115
+ cache[`${name}@${version}`] = { report, timestamp: Date.now() };
116
+ writeCache(cache);
117
+ }
118
+ async function fetchNpmMetadata(packageName) {
119
+ const timeout = 1e4;
120
+ const signal = AbortSignal.timeout(timeout);
121
+ try {
122
+ const [regRes, dlRes] = await Promise.all([
123
+ fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`, { signal }),
124
+ fetch(`https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(packageName)}`, {
125
+ signal: AbortSignal.timeout(timeout)
126
+ })
127
+ ]);
128
+ if (!regRes.ok) return null;
129
+ const reg = await regRes.json();
130
+ const time = reg["time"] ?? {};
131
+ const created = time["created"] ? new Date(time["created"]) : null;
132
+ const modified = time["modified"] ? new Date(time["modified"]) : null;
133
+ const packageAge = created ? Math.floor((Date.now() - created.getTime()) / 864e5) : 0;
134
+ const maintainers = Array.isArray(reg["maintainers"]) ? reg["maintainers"] : [];
135
+ const latestVersion = reg["dist-tags"]?.["latest"] ?? "";
136
+ const versionData = reg["versions"]?.[latestVersion];
137
+ const deprecated = typeof versionData?.["deprecated"] === "string";
138
+ let weeklyDownloads = 0;
139
+ if (dlRes.ok) {
140
+ const dl = await dlRes.json();
141
+ weeklyDownloads = typeof dl["downloads"] === "number" ? dl["downloads"] : 0;
142
+ }
143
+ return {
144
+ weeklyDownloads,
145
+ lastPublish: modified?.toISOString() ?? (/* @__PURE__ */ new Date()).toISOString(),
146
+ packageAge,
147
+ maintainerCount: maintainers.length,
148
+ deprecated
149
+ };
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+ async function fetchVulnerabilities(packageName, version) {
155
+ try {
156
+ const res = await fetch("https://api.osv.dev/v1/query", {
157
+ method: "POST",
158
+ headers: { "Content-Type": "application/json" },
159
+ body: JSON.stringify({ package: { name: packageName, ecosystem: "npm" }, version }),
160
+ signal: AbortSignal.timeout(1e4)
161
+ });
162
+ if (!res.ok) return [];
163
+ const data = await res.json();
164
+ const vulns = Array.isArray(data["vulns"]) ? data["vulns"] : [];
165
+ return vulns.map((v) => {
166
+ const severity = v["database_specific"]?.["severity"];
167
+ const sev = typeof severity === "string" ? severity.toLowerCase() : "moderate";
168
+ const refs = Array.isArray(v["references"]) ? v["references"] : [];
169
+ return {
170
+ severity: ["low", "moderate", "high", "critical"].includes(sev) ? sev : "moderate",
171
+ title: typeof v["summary"] === "string" ? v["summary"] : typeof v["id"] === "string" ? v["id"] : "Unknown vulnerability",
172
+ url: typeof refs[0]?.["url"] === "string" ? refs[0]["url"] : void 0
173
+ };
174
+ });
175
+ } catch {
176
+ return [];
177
+ }
178
+ }
179
+ async function scanServer(name, entry) {
180
+ if (entry.source !== "npm") {
181
+ return {
182
+ server: name,
183
+ source: entry.source,
184
+ score: null,
185
+ riskLevel: "UNKNOWN",
186
+ vulnerabilities: [],
187
+ metadata: null
188
+ };
189
+ }
190
+ const cached = getCachedReport(name, entry.version);
191
+ if (cached) return cached;
192
+ const [metadata, vulnerabilities] = await Promise.all([
193
+ fetchNpmMetadata(name),
194
+ fetchVulnerabilities(name, entry.version)
195
+ ]);
196
+ const { computeTrustScore } = await import("./trust-scorer-LYC6KZCD.js");
197
+ const { score, riskLevel } = computeTrustScore(metadata, vulnerabilities);
198
+ const report = {
199
+ server: name,
200
+ source: "npm",
201
+ score,
202
+ riskLevel,
203
+ vulnerabilities,
204
+ metadata
205
+ };
206
+ cacheReport(name, entry.version, report);
207
+ return report;
208
+ }
209
+ async function scanAllServers(servers, concurrency = 3) {
210
+ const entries = Object.entries(servers);
211
+ const results = [];
212
+ const executing = /* @__PURE__ */ new Set();
213
+ for (const [name, entry] of entries) {
214
+ const p10 = scanServer(name, entry).then((r) => {
215
+ results.push(r);
216
+ executing.delete(p10);
217
+ });
218
+ executing.add(p10);
219
+ if (executing.size >= concurrency) await Promise.race(executing);
220
+ }
221
+ await Promise.all(executing);
222
+ return results;
223
+ }
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
+
532
+ // src/commands/audit.ts
533
+ function colorRisk(level, score) {
534
+ const label = score !== null ? `${score}/100 (${level})` : level;
535
+ if (level === "LOW") return pc.green(label);
536
+ if (level === "MEDIUM") return pc.yellow(label);
537
+ if (level === "HIGH") return pc.red(label);
538
+ if (level === "CRITICAL") return pc.bold(pc.red(label));
539
+ return pc.dim(label);
540
+ }
541
+ function daysAgo(isoDate) {
542
+ const days = Math.floor((Date.now() - new Date(isoDate).getTime()) / 864e5);
543
+ if (days === 0) return "today";
544
+ if (days === 1) return "1 day ago";
545
+ return `${days} days ago`;
546
+ }
547
+ function countVulns(vulns) {
548
+ const c = { critical: 0, high: 0, moderate: 0, low: 0 };
549
+ for (const v of vulns) c[v.severity]++;
550
+ if (vulns.length === 0) return pc.green("none");
551
+ const parts = [];
552
+ if (c.critical) parts.push(pc.bold(pc.red(`${c.critical} critical`)));
553
+ if (c.high) parts.push(pc.red(`${c.high} high`));
554
+ if (c.moderate) parts.push(pc.yellow(`${c.moderate} moderate`));
555
+ if (c.low) parts.push(pc.dim(`${c.low} low`));
556
+ return parts.join(", ");
557
+ }
558
+ function printReport(report) {
559
+ const riskColored = colorRisk(report.riskLevel, report.score);
560
+ const icon = report.riskLevel === "LOW" ? pc.green("\u25CF") : report.riskLevel === "MEDIUM" ? pc.yellow("\u25CF") : report.riskLevel === "UNKNOWN" ? pc.dim("\u25CB") : pc.red("\u25CF");
561
+ console.log(` ${icon} ${pc.bold(report.server)} Score: ${riskColored}`);
562
+ if (report.source !== "npm") {
563
+ console.log(` ${pc.dim("Non-npm source \u2014 security data unavailable")}`);
564
+ console.log();
565
+ return;
566
+ }
567
+ if (report.metadata) {
568
+ const { weeklyDownloads, packageAge, lastPublish, maintainerCount, deprecated } = report.metadata;
569
+ const dlStr = weeklyDownloads.toLocaleString();
570
+ console.log(
571
+ ` ${pc.dim("Downloads:")} ${dlStr}/week ${pc.dim("|")} ${pc.dim("Age:")} ${packageAge}d ${pc.dim("|")} ${pc.dim("Last publish:")} ${daysAgo(lastPublish)} ${pc.dim("|")} ${pc.dim("Maintainers:")} ${maintainerCount}` + (deprecated ? pc.red(" [DEPRECATED]") : "")
572
+ );
573
+ }
574
+ console.log(` ${pc.dim("Vulnerabilities:")} ${countVulns(report.vulnerabilities)}`);
575
+ if (report.vulnerabilities.length > 0) {
576
+ for (const v of report.vulnerabilities) {
577
+ const sevColor = v.severity === "critical" || v.severity === "high" ? pc.red : pc.yellow;
578
+ const url = v.url ? pc.dim(` ${v.url}`) : "";
579
+ console.log(` ${sevColor("\u25B8")} [${v.severity}] ${v.title}${url}`);
580
+ }
581
+ }
582
+ console.log();
583
+ }
584
+ var audit_default = defineCommand({
585
+ meta: {
586
+ name: "audit",
587
+ description: "Scan installed MCP servers for security vulnerabilities and trust scores"
588
+ },
589
+ args: {
590
+ server: {
591
+ type: "positional",
592
+ description: "Specific server to audit (omit to audit all)",
593
+ required: false
594
+ },
595
+ json: {
596
+ type: "boolean",
597
+ description: "Output results as JSON",
598
+ default: false
599
+ },
600
+ fix: {
601
+ type: "boolean",
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)",
608
+ default: false
609
+ }
610
+ },
611
+ async run({ args }) {
612
+ const lockfile = readLockfile();
613
+ const { servers } = lockfile;
614
+ if (Object.keys(servers).length === 0) {
615
+ console.log(pc.dim("\n No MCP servers installed. Run mcpman install <server> to get started.\n"));
616
+ return;
617
+ }
618
+ const targets = {};
619
+ if (args.server) {
620
+ if (!servers[args.server]) {
621
+ console.error(pc.red(`
622
+ Server "${args.server}" not found in lockfile.
623
+ `));
624
+ process.exit(1);
625
+ }
626
+ targets[args.server] = servers[args.server];
627
+ } else {
628
+ Object.assign(targets, servers);
629
+ }
630
+ const spinner5 = createSpinner(`Scanning ${Object.keys(targets).length} server(s)...`).start();
631
+ let reports;
632
+ try {
633
+ reports = args.server ? [await scanServer(args.server, targets[args.server])] : await scanAllServers(targets);
634
+ } catch (err) {
635
+ spinner5.error({ text: "Scan failed" });
636
+ console.error(pc.red(String(err)));
637
+ process.exit(1);
638
+ }
639
+ spinner5.success({ text: `Scanned ${reports.length} server(s)` });
640
+ if (args.json) {
641
+ console.log(JSON.stringify(reports, null, 2));
642
+ return;
643
+ }
644
+ console.log(pc.bold("\n mcpman audit\n"));
645
+ console.log(pc.dim(" " + "\u2500".repeat(60)));
646
+ for (const report of reports) {
647
+ printReport(report);
648
+ }
649
+ console.log(pc.dim(" " + "\u2500".repeat(60)));
650
+ const withIssues = reports.filter(
651
+ (r) => r.riskLevel !== "LOW" && r.riskLevel !== "UNKNOWN"
652
+ );
653
+ const npmReports = reports.filter((r) => r.source === "npm");
654
+ const parts = [];
655
+ parts.push(`${reports.length} server(s) scanned`);
656
+ if (npmReports.length < reports.length) {
657
+ parts.push(pc.dim(`${reports.length - npmReports.length} non-npm (unverified)`));
658
+ }
659
+ if (withIssues.length > 0) {
660
+ parts.push(pc.yellow(`${withIssues.length} with issues`));
661
+ } else {
662
+ parts.push(pc.green("all clear"));
663
+ }
664
+ console.log(`
665
+ Summary: ${parts.join(" | ")}
666
+ `);
667
+ if (args.fix) {
668
+ await runAuditFix(reports, lockfile.servers, args.yes);
669
+ }
670
+ }
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
+ }
8
772
 
9
773
  // src/commands/doctor.ts
10
- import { defineCommand } from "citty";
11
- import pc from "picocolors";
774
+ import { defineCommand as defineCommand2 } from "citty";
775
+ import pc2 from "picocolors";
12
776
 
13
777
  // src/core/server-inventory.ts
14
778
  async function getInstalledServers(clientFilter) {
@@ -272,12 +1036,12 @@ async function quickHealthProbe(config, timeoutMs = 3e3) {
272
1036
 
273
1037
  // src/commands/doctor.ts
274
1038
  var CHECK_ICON = {
275
- pass: pc.green("\u2713"),
276
- fail: pc.red("\u2717"),
277
- skip: pc.dim("-"),
278
- warn: pc.yellow("\u26A0")
1039
+ pass: pc2.green("\u2713"),
1040
+ fail: pc2.red("\u2717"),
1041
+ skip: pc2.dim("-"),
1042
+ warn: pc2.yellow("\u26A0")
279
1043
  };
280
- var doctor_default = defineCommand({
1044
+ var doctor_default = defineCommand2({
281
1045
  meta: {
282
1046
  name: "doctor",
283
1047
  description: "Check MCP server health and configuration"
@@ -290,10 +1054,10 @@ var doctor_default = defineCommand({
290
1054
  }
291
1055
  },
292
1056
  async run({ args }) {
293
- console.log(pc.bold("\n mcpman doctor\n"));
1057
+ console.log(pc2.bold("\n mcpman doctor\n"));
294
1058
  const servers = await getInstalledServers();
295
1059
  if (servers.length === 0) {
296
- console.log(pc.dim(" No MCP servers installed. Run mcpman install <server> to get started."));
1060
+ console.log(pc2.dim(" No MCP servers installed. Run mcpman install <server> to get started."));
297
1061
  return;
298
1062
  }
299
1063
  const tasks = servers.map((s) => () => checkServerHealth(s.name, s.config));
@@ -305,14 +1069,14 @@ var doctor_default = defineCommand({
305
1069
  if (result.status === "healthy") passed++;
306
1070
  else failed++;
307
1071
  }
308
- console.log(pc.dim(" " + "\u2500".repeat(50)));
1072
+ console.log(pc2.dim(" " + "\u2500".repeat(50)));
309
1073
  const parts = [];
310
- if (passed > 0) parts.push(pc.green(`${passed} healthy`));
311
- if (failed > 0) parts.push(pc.red(`${failed} unhealthy`));
1074
+ if (passed > 0) parts.push(pc2.green(`${passed} healthy`));
1075
+ if (failed > 0) parts.push(pc2.red(`${failed} unhealthy`));
312
1076
  console.log(` Summary: ${parts.join(", ")}`);
313
1077
  if (failed > 0) {
314
1078
  if (!args.fix) {
315
- console.log(pc.dim(` Run ${pc.cyan("mcpman doctor --fix")} for fix suggestions.
1079
+ console.log(pc2.dim(` Run ${pc2.cyan("mcpman doctor --fix")} for fix suggestions.
316
1080
  `));
317
1081
  }
318
1082
  process.exit(1);
@@ -321,13 +1085,13 @@ var doctor_default = defineCommand({
321
1085
  }
322
1086
  });
323
1087
  function printServerResult(result, showFix) {
324
- const icon = result.status === "healthy" ? pc.green("\u25CF") : pc.red("\u25CF");
325
- console.log(` ${icon} ${pc.bold(result.serverName)}`);
1088
+ const icon = result.status === "healthy" ? pc2.green("\u25CF") : pc2.red("\u25CF");
1089
+ console.log(` ${icon} ${pc2.bold(result.serverName)}`);
326
1090
  for (const check of result.checks) {
327
1091
  const checkIcon = check.skipped ? CHECK_ICON.skip : check.passed ? CHECK_ICON.pass : CHECK_ICON.fail;
328
1092
  console.log(` ${checkIcon} ${check.name}: ${check.message}`);
329
1093
  if (showFix && !check.passed && !check.skipped && check.fix) {
330
- console.log(` ${pc.yellow("\u2192")} Fix: ${pc.cyan(check.fix)}`);
1094
+ console.log(` ${pc2.yellow("\u2192")} Fix: ${pc2.cyan(check.fix)}`);
331
1095
  }
332
1096
  }
333
1097
  console.log();
@@ -336,11 +1100,11 @@ async function runParallel(tasks, concurrency) {
336
1100
  const results = [];
337
1101
  const executing = /* @__PURE__ */ new Set();
338
1102
  for (const task of tasks) {
339
- const p5 = task().then((r) => {
1103
+ const p10 = task().then((r) => {
340
1104
  results.push(r);
341
- executing.delete(p5);
1105
+ executing.delete(p10);
342
1106
  });
343
- executing.add(p5);
1107
+ executing.add(p10);
344
1108
  if (executing.size >= concurrency) {
345
1109
  await Promise.race(executing);
346
1110
  }
@@ -350,182 +1114,10 @@ async function runParallel(tasks, concurrency) {
350
1114
  }
351
1115
 
352
1116
  // src/commands/init.ts
353
- import { defineCommand as defineCommand2 } from "citty";
354
- import * as p from "@clack/prompts";
355
- import path2 from "path";
356
-
357
- // src/core/lockfile.ts
358
- import fs from "fs";
359
- import path from "path";
360
- import os from "os";
361
- var LOCKFILE_NAME = "mcpman.lock";
362
- function findLockfile() {
363
- let dir = process.cwd();
364
- while (true) {
365
- const candidate = path.join(dir, LOCKFILE_NAME);
366
- if (fs.existsSync(candidate)) return candidate;
367
- const parent = path.dirname(dir);
368
- if (parent === dir) break;
369
- dir = parent;
370
- }
371
- return null;
372
- }
373
- function getGlobalLockfilePath() {
374
- return path.join(os.homedir(), ".mcpman", LOCKFILE_NAME);
375
- }
376
- function resolveLockfilePath() {
377
- return findLockfile() ?? getGlobalLockfilePath();
378
- }
379
- function readLockfile(filePath) {
380
- const target = filePath ?? resolveLockfilePath();
381
- if (!fs.existsSync(target)) {
382
- return { lockfileVersion: 1, servers: {} };
383
- }
384
- try {
385
- const raw = fs.readFileSync(target, "utf-8");
386
- return JSON.parse(raw);
387
- } catch {
388
- return { lockfileVersion: 1, servers: {} };
389
- }
390
- }
391
- function serialize(data) {
392
- const sorted = {
393
- lockfileVersion: data.lockfileVersion,
394
- servers: Object.fromEntries(
395
- Object.entries(data.servers).sort(([a], [b]) => a.localeCompare(b))
396
- )
397
- };
398
- return JSON.stringify(sorted, null, 2) + "\n";
399
- }
400
- function writeLockfile(data, filePath) {
401
- const target = filePath ?? resolveLockfilePath();
402
- const dir = path.dirname(target);
403
- if (!fs.existsSync(dir)) {
404
- fs.mkdirSync(dir, { recursive: true });
405
- }
406
- const tmp = `${target}.tmp`;
407
- fs.writeFileSync(tmp, serialize(data), "utf-8");
408
- fs.renameSync(tmp, target);
409
- }
410
- function addEntry(name, entry, filePath) {
411
- const data = readLockfile(filePath);
412
- data.servers[name] = entry;
413
- writeLockfile(data, filePath);
414
- }
415
- function createEmptyLockfile(filePath) {
416
- writeLockfile({ lockfileVersion: 1, servers: {} }, filePath);
417
- }
418
-
419
- // src/core/registry.ts
420
- import { createHash } from "crypto";
421
- function computeIntegrity(resolvedUrl) {
422
- const hash = createHash("sha512").update(resolvedUrl).digest("base64");
423
- return `sha512-${hash}`;
424
- }
425
- async function resolveFromSmithery(name) {
426
- const url = `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`;
427
- let data;
428
- try {
429
- const res = await fetch(url, {
430
- headers: { Accept: "application/json" },
431
- signal: AbortSignal.timeout(8e3)
432
- });
433
- if (res.status === 404) {
434
- throw new Error(`Server '${name}' not found on Smithery registry`);
435
- }
436
- if (!res.ok) {
437
- throw new Error(`Smithery API error: ${res.status}`);
438
- }
439
- data = await res.json();
440
- } catch (err) {
441
- if (err instanceof Error && err.message.includes("not found")) throw err;
442
- throw new Error(
443
- `Cannot reach Smithery registry: ${err instanceof Error ? err.message : String(err)}`
444
- );
445
- }
446
- const version = typeof data.version === "string" ? data.version : "latest";
447
- const command = typeof data.command === "string" ? data.command : "npx";
448
- const args = Array.isArray(data.args) ? data.args : ["-y", `${name}@${version}`];
449
- const envVars = Array.isArray(data.envVars) ? data.envVars : [];
450
- const resolved = typeof data.resolved === "string" ? data.resolved : `smithery:${name}@${version}`;
451
- return {
452
- name,
453
- version,
454
- description: typeof data.description === "string" ? data.description : "",
455
- runtime: "node",
456
- command,
457
- args,
458
- envVars,
459
- resolved
460
- };
461
- }
462
- async function resolveFromNpm(packageName) {
463
- const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
464
- let data;
465
- try {
466
- const res = await fetch(url, {
467
- headers: { Accept: "application/json" },
468
- signal: AbortSignal.timeout(8e3)
469
- });
470
- if (res.status === 404) {
471
- throw new Error(`Package '${packageName}' not found on npm`);
472
- }
473
- if (!res.ok) {
474
- throw new Error(`npm registry error: ${res.status}`);
475
- }
476
- data = await res.json();
477
- } catch (err) {
478
- if (err instanceof Error && err.message.includes("not found")) throw err;
479
- throw new Error(
480
- `Cannot reach npm registry: ${err instanceof Error ? err.message : String(err)}`
481
- );
482
- }
483
- const version = typeof data.version === "string" ? data.version : "latest";
484
- const resolved = `https://registry.npmjs.org/${packageName}/-/${packageName.replace(/^@[^/]+\//, "")}-${version}.tgz`;
485
- const mcpField = data.mcp && typeof data.mcp === "object" ? data.mcp : null;
486
- const envVars = mcpField?.envVars ? mcpField.envVars : [];
487
- return {
488
- name: packageName,
489
- version,
490
- description: typeof data.description === "string" ? data.description : "",
491
- runtime: "node",
492
- command: "npx",
493
- args: ["-y", `${packageName}@${version}`],
494
- envVars,
495
- resolved
496
- };
497
- }
498
- async function resolveFromGitHub(githubUrl) {
499
- const match = githubUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
500
- if (!match) {
501
- throw new Error(`Invalid GitHub URL: ${githubUrl}`);
502
- }
503
- const [, owner, repo] = match;
504
- const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/package.json`;
505
- let pkgData = {};
506
- try {
507
- const res = await fetch(rawUrl, { signal: AbortSignal.timeout(8e3) });
508
- if (res.ok) {
509
- pkgData = await res.json();
510
- }
511
- } catch {
512
- }
513
- const version = typeof pkgData.version === "string" ? pkgData.version : "main";
514
- const name = typeof pkgData.name === "string" ? pkgData.name : `${owner}/${repo}`;
515
- return {
516
- name,
517
- version,
518
- description: typeof pkgData.description === "string" ? pkgData.description : "",
519
- runtime: "node",
520
- command: "npx",
521
- args: ["-y", githubUrl],
522
- envVars: [],
523
- resolved: githubUrl
524
- };
525
- }
526
-
527
- // src/commands/init.ts
528
- var init_default = defineCommand2({
1117
+ import { defineCommand as defineCommand3 } from "citty";
1118
+ import * as p2 from "@clack/prompts";
1119
+ import path3 from "path";
1120
+ var init_default = defineCommand3({
529
1121
  meta: {
530
1122
  name: "init",
531
1123
  description: "Initialize mcpman.lock in the current project"
@@ -540,17 +1132,17 @@ var init_default = defineCommand2({
540
1132
  },
541
1133
  async run({ args }) {
542
1134
  const nonInteractive = args.yes || !process.stdout.isTTY;
543
- p.intro("mcpman init");
544
- const targetPath = path2.join(process.cwd(), LOCKFILE_NAME);
1135
+ p2.intro("mcpman init");
1136
+ const targetPath = path3.join(process.cwd(), LOCKFILE_NAME);
545
1137
  const existing = findLockfile();
546
1138
  if (existing) {
547
1139
  if (nonInteractive) {
548
- p.log.warn(`Lockfile already exists: ${existing} \u2014 overwriting (non-interactive).`);
1140
+ p2.log.warn(`Lockfile already exists: ${existing} \u2014 overwriting (non-interactive).`);
549
1141
  } else {
550
- p.log.warn(`Lockfile already exists: ${existing}`);
551
- const overwrite = await p.confirm({ message: "Overwrite?" });
552
- if (p.isCancel(overwrite) || !overwrite) {
553
- p.outro("Cancelled.");
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.");
554
1146
  return;
555
1147
  }
556
1148
  }
@@ -560,7 +1152,7 @@ var init_default = defineCommand2({
560
1152
  const mod = await import("./client-detector-SUIJSIYM.js");
561
1153
  clients = await mod.getInstalledClients();
562
1154
  } catch {
563
- p.log.warn("Could not detect AI clients \u2014 creating empty lockfile.");
1155
+ p2.log.warn("Could not detect AI clients \u2014 creating empty lockfile.");
564
1156
  }
565
1157
  const clientServers = [];
566
1158
  for (const client of clients) {
@@ -574,26 +1166,26 @@ var init_default = defineCommand2({
574
1166
  }
575
1167
  createEmptyLockfile(targetPath);
576
1168
  if (clientServers.length === 0) {
577
- p.log.info("No existing servers found in any client config.");
578
- p.outro(`Created ${LOCKFILE_NAME} \u2014 add it to version control!`);
1169
+ p2.log.info("No existing servers found in any client config.");
1170
+ p2.outro(`Created ${LOCKFILE_NAME} \u2014 add it to version control!`);
579
1171
  return;
580
1172
  }
581
1173
  let selected;
582
1174
  if (nonInteractive) {
583
1175
  selected = clientServers.map((cs) => cs.client.type);
584
- p.log.info(`Non-interactive mode: importing all ${clientServers.length} client(s).`);
1176
+ p2.log.info(`Non-interactive mode: importing all ${clientServers.length} client(s).`);
585
1177
  } else {
586
1178
  const options = clientServers.map((cs) => ({
587
1179
  value: cs.client.type,
588
1180
  label: `${cs.client.displayName} (${Object.keys(cs.servers).length} servers)`
589
1181
  }));
590
- const toImport = await p.multiselect({
1182
+ const toImport = await p2.multiselect({
591
1183
  message: "Import existing servers into lockfile?",
592
1184
  options,
593
1185
  required: false
594
1186
  });
595
- if (p.isCancel(toImport)) {
596
- p.outro(`Created empty ${LOCKFILE_NAME}`);
1187
+ if (p2.isCancel(toImport)) {
1188
+ p2.outro(`Created empty ${LOCKFILE_NAME}`);
597
1189
  return;
598
1190
  }
599
1191
  selected = toImport;
@@ -619,54 +1211,52 @@ var init_default = defineCommand2({
619
1211
  importCount++;
620
1212
  }
621
1213
  }
622
- p.outro(
1214
+ p2.outro(
623
1215
  `Created ${LOCKFILE_NAME} with ${importCount} server(s) \u2014 commit to version control!`
624
1216
  );
625
1217
  }
626
1218
  });
627
1219
 
628
1220
  // src/commands/install.ts
629
- import { defineCommand as defineCommand3 } from "citty";
1221
+ import { defineCommand as defineCommand4 } from "citty";
630
1222
 
631
1223
  // src/core/installer.ts
632
- import * as p2 from "@clack/prompts";
1224
+ import * as p4 from "@clack/prompts";
633
1225
 
634
- // src/core/server-resolver.ts
635
- function detectSource(input) {
636
- if (input.startsWith("smithery:")) {
637
- return { type: "smithery", input: input.slice(9) };
638
- }
639
- if (input.startsWith("https://github.com/") || input.startsWith("github.com/")) {
640
- return { type: "github", input };
641
- }
642
- return { type: "npm", input };
643
- }
644
- function parseEnvFlags(envFlags) {
645
- if (!envFlags) return {};
646
- const flags = Array.isArray(envFlags) ? envFlags : [envFlags];
647
- const result = {};
648
- for (const flag of flags) {
649
- const idx = flag.indexOf("=");
650
- if (idx > 0) {
651
- 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 {};
652
1233
  }
1234
+ const password = await getMasterPassword();
1235
+ return getSecretsForServer(serverName, password);
1236
+ } catch {
1237
+ return {};
653
1238
  }
654
- return result;
655
1239
  }
656
- async function resolveServer(input) {
657
- const source = detectSource(input);
658
- switch (source.type) {
659
- case "smithery":
660
- return resolveFromSmithery(source.input);
661
- case "github":
662
- return resolveFromGitHub(source.input);
663
- case "npm":
664
- return resolveFromNpm(source.input);
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)}`);
665
1255
  }
666
1256
  }
667
1257
 
668
1258
  // src/core/installer.ts
669
- async function loadClients() {
1259
+ async function loadClients2() {
670
1260
  try {
671
1261
  const mod = await import("./client-detector-SUIJSIYM.js");
672
1262
  return mod.getInstalledClients();
@@ -675,83 +1265,86 @@ async function loadClients() {
675
1265
  }
676
1266
  }
677
1267
  async function installServer(input, options = {}) {
678
- p2.intro("mcpman install");
679
- const spinner2 = p2.spinner();
680
- spinner2.start("Resolving server...");
1268
+ p4.intro("mcpman install");
1269
+ const spinner5 = p4.spinner();
1270
+ spinner5.start("Resolving server...");
681
1271
  let metadata;
682
1272
  try {
683
1273
  metadata = await resolveServer(input);
684
1274
  } catch (err) {
685
- spinner2.stop("Resolution failed");
686
- p2.log.error(err instanceof Error ? err.message : String(err));
1275
+ spinner5.stop("Resolution failed");
1276
+ p4.log.error(err instanceof Error ? err.message : String(err));
687
1277
  process.exit(1);
688
1278
  }
689
- spinner2.stop(`Found: ${metadata.name}@${metadata.version}`);
690
- const clients = await loadClients();
1279
+ spinner5.stop(`Found: ${metadata.name}@${metadata.version}`);
1280
+ const clients = await loadClients2();
691
1281
  if (clients.length === 0) {
692
- p2.log.warn("No supported AI clients detected on this machine.");
693
- p2.log.info("Supported: Claude Desktop, Cursor, VS Code, Windsurf");
1282
+ p4.log.warn("No supported AI clients detected on this machine.");
1283
+ p4.log.info("Supported: Claude Desktop, Cursor, VS Code, Windsurf");
694
1284
  process.exit(1);
695
1285
  }
696
1286
  let selectedClients;
697
1287
  if (options.client) {
698
1288
  const found = clients.find((c) => c.type === options.client || c.displayName.toLowerCase() === options.client?.toLowerCase());
699
1289
  if (!found) {
700
- p2.log.error(`Client '${options.client}' not found or not installed.`);
701
- p2.log.info(`Available: ${clients.map((c) => c.type).join(", ")}`);
1290
+ p4.log.error(`Client '${options.client}' not found or not installed.`);
1291
+ p4.log.info(`Available: ${clients.map((c) => c.type).join(", ")}`);
702
1292
  process.exit(1);
703
1293
  }
704
1294
  selectedClients = [found];
705
1295
  } else if (options.yes || clients.length === 1) {
706
1296
  selectedClients = clients;
707
1297
  } else {
708
- const chosen = await p2.multiselect({
1298
+ const chosen = await p4.multiselect({
709
1299
  message: "Install to which client(s)?",
710
1300
  options: clients.map((c) => ({ value: c.type, label: c.displayName })),
711
1301
  required: true
712
1302
  });
713
- if (p2.isCancel(chosen)) {
714
- p2.outro("Cancelled.");
1303
+ if (p4.isCancel(chosen)) {
1304
+ p4.outro("Cancelled.");
715
1305
  process.exit(0);
716
1306
  }
717
1307
  selectedClients = clients.filter((c) => chosen.includes(c.type));
718
1308
  }
719
1309
  const providedEnv = parseEnvFlags(options.env);
720
- const collectedEnv = { ...providedEnv };
1310
+ const vaultEnv = await tryLoadVaultSecrets(metadata.name);
1311
+ const collectedEnv = { ...vaultEnv, ...providedEnv };
1312
+ const newlyEnteredVars = {};
721
1313
  const requiredVars = metadata.envVars.filter((e) => e.required && !(e.name in collectedEnv));
722
1314
  for (const envVar of requiredVars) {
723
1315
  if (options.yes && envVar.default) {
724
1316
  collectedEnv[envVar.name] = envVar.default;
725
1317
  continue;
726
1318
  }
727
- const val = await p2.text({
1319
+ const val = await p4.text({
728
1320
  message: `${envVar.name}${envVar.description ? ` \u2014 ${envVar.description}` : ""}`,
729
1321
  placeholder: envVar.default ?? "",
730
1322
  validate: (v) => envVar.required && !v ? "Required" : void 0
731
1323
  });
732
- if (p2.isCancel(val)) {
733
- p2.outro("Cancelled.");
1324
+ if (p4.isCancel(val)) {
1325
+ p4.outro("Cancelled.");
734
1326
  process.exit(0);
735
1327
  }
736
1328
  collectedEnv[envVar.name] = val;
1329
+ newlyEnteredVars[envVar.name] = val;
737
1330
  }
738
1331
  const entry = {
739
1332
  command: metadata.command,
740
1333
  args: metadata.args,
741
1334
  ...Object.keys(collectedEnv).length > 0 ? { env: collectedEnv } : {}
742
1335
  };
743
- spinner2.start("Writing config...");
1336
+ spinner5.start("Writing config...");
744
1337
  const clientTypes = [];
745
1338
  for (const client of selectedClients) {
746
1339
  try {
747
1340
  await client.addServer(metadata.name, entry);
748
1341
  clientTypes.push(client.type);
749
1342
  } catch (err) {
750
- spinner2.stop("Partial failure");
751
- p2.log.warn(`Failed to write to ${client.displayName}: ${err instanceof Error ? err.message : String(err)}`);
1343
+ spinner5.stop("Partial failure");
1344
+ p4.log.warn(`Failed to write to ${client.displayName}: ${err instanceof Error ? err.message : String(err)}`);
752
1345
  }
753
1346
  }
754
- spinner2.stop("Config written");
1347
+ spinner5.stop("Config written");
755
1348
  const source = detectSource(input);
756
1349
  const integrity = computeIntegrity(metadata.resolved);
757
1350
  addEntry(metadata.name, {
@@ -767,12 +1360,13 @@ async function installServer(input, options = {}) {
767
1360
  clients: clientTypes
768
1361
  });
769
1362
  const lockPath = findLockfile() ?? "mcpman.lock (global)";
770
- p2.log.success(`Lockfile updated: ${lockPath}`);
771
- p2.outro(`${metadata.name}@${metadata.version} installed to ${clientTypes.join(", ")}`);
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(", ")}`);
772
1366
  }
773
1367
 
774
1368
  // src/utils/logger.ts
775
- import pc2 from "picocolors";
1369
+ import pc3 from "picocolors";
776
1370
  var noColor = process.env.NO_COLOR !== void 0 || process.argv.includes("--no-color");
777
1371
  var isVerbose = process.argv.includes("--verbose");
778
1372
  var isJson = process.argv.includes("--json");
@@ -781,19 +1375,19 @@ function colorize(fn, text2) {
781
1375
  }
782
1376
  function info(message) {
783
1377
  if (isJson) return;
784
- console.log(`${colorize(pc2.cyan, "i")} ${message}`);
1378
+ console.log(`${colorize(pc3.cyan, "i")} ${message}`);
785
1379
  }
786
1380
  function error(message) {
787
1381
  if (isJson) return;
788
- console.error(`${colorize(pc2.red, "\u2717")} ${message}`);
1382
+ console.error(`${colorize(pc3.red, "\u2717")} ${message}`);
789
1383
  }
790
1384
  function json(data) {
791
1385
  console.log(JSON.stringify(data, null, 2));
792
1386
  }
793
1387
 
794
1388
  // src/commands/install.ts
795
- import * as p3 from "@clack/prompts";
796
- var install_default = defineCommand3({
1389
+ import * as p5 from "@clack/prompts";
1390
+ var install_default = defineCommand4({
797
1391
  meta: {
798
1392
  name: "install",
799
1393
  description: "Install an MCP server into one or more AI clients"
@@ -842,8 +1436,8 @@ async function restoreFromLockfile() {
842
1436
  info("Lockfile is empty \u2014 nothing to restore.");
843
1437
  return;
844
1438
  }
845
- p3.intro(`mcpman install (restore from ${lockPath})`);
846
- p3.log.info(`Restoring ${entries.length} server(s)...`);
1439
+ p5.intro(`mcpman install (restore from ${lockPath})`);
1440
+ p5.log.info(`Restoring ${entries.length} server(s)...`);
847
1441
  for (const [name, entry] of entries) {
848
1442
  const input = entry.source === "smithery" ? `smithery:${name}` : entry.source === "github" ? entry.resolved : name;
849
1443
  await installServer(input, {
@@ -851,18 +1445,18 @@ async function restoreFromLockfile() {
851
1445
  yes: true
852
1446
  });
853
1447
  }
854
- p3.outro("Restore complete.");
1448
+ p5.outro("Restore complete.");
855
1449
  }
856
1450
 
857
1451
  // src/commands/list.ts
858
- import { defineCommand as defineCommand4 } from "citty";
859
- import pc3 from "picocolors";
1452
+ import { defineCommand as defineCommand5 } from "citty";
1453
+ import pc4 from "picocolors";
860
1454
  var STATUS_ICON = {
861
- healthy: pc3.green("\u25CF"),
862
- unhealthy: pc3.red("\u25CF"),
863
- unknown: pc3.dim("\u25CB")
1455
+ healthy: pc4.green("\u25CF"),
1456
+ unhealthy: pc4.red("\u25CF"),
1457
+ unknown: pc4.dim("\u25CB")
864
1458
  };
865
- var list_default = defineCommand4({
1459
+ var list_default = defineCommand5({
866
1460
  meta: {
867
1461
  name: "list",
868
1462
  description: "List installed MCP servers"
@@ -882,7 +1476,7 @@ var list_default = defineCommand4({
882
1476
  const servers = await getInstalledServers(args.client);
883
1477
  if (servers.length === 0) {
884
1478
  const filter = args.client ? ` for client "${args.client}"` : "";
885
- console.log(pc3.dim(`No MCP servers installed${filter}. Run ${pc3.cyan("mcpman install <server>")} to get started.`));
1479
+ console.log(pc4.dim(`No MCP servers installed${filter}. Run ${pc4.cyan("mcpman install <server>")} to get started.`));
886
1480
  return;
887
1481
  }
888
1482
  const withStatus = await Promise.all(
@@ -906,8 +1500,8 @@ var list_default = defineCommand4({
906
1500
  const nameWidth = Math.max(4, ...withStatus.map((s) => s.name.length));
907
1501
  const clientsWidth = Math.max(7, ...withStatus.map((s) => formatClients(s.clients).length));
908
1502
  const header = ` ${pad("NAME", nameWidth)} ${pad("CLIENT(S)", clientsWidth)} ${pad("COMMAND", 20)} STATUS`;
909
- console.log(pc3.dim(header));
910
- console.log(pc3.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientsWidth)} ${"-".repeat(20)} ------`));
1503
+ console.log(pc4.dim(header));
1504
+ console.log(pc4.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientsWidth)} ${"-".repeat(20)} ------`));
911
1505
  for (const s of withStatus) {
912
1506
  const icon = STATUS_ICON[s.status];
913
1507
  const clientsStr = formatClients(s.clients);
@@ -915,7 +1509,7 @@ var list_default = defineCommand4({
915
1509
  console.log(` ${pad(s.name, nameWidth)} ${pad(clientsStr, clientsWidth)} ${pad(cmdStr, 20)} ${icon} ${s.status}`);
916
1510
  }
917
1511
  const clientSet = new Set(withStatus.flatMap((s) => s.clients));
918
- console.log(pc3.dim(`
1512
+ console.log(pc4.dim(`
919
1513
  ${withStatus.length} server${withStatus.length !== 1 ? "s" : ""} \xB7 ${clientSet.size} client${clientSet.size !== 1 ? "s" : ""}`));
920
1514
  }
921
1515
  });
@@ -936,9 +1530,9 @@ function formatClients(clients) {
936
1530
  }
937
1531
 
938
1532
  // src/commands/remove.ts
939
- import { defineCommand as defineCommand5 } from "citty";
940
- import * as p4 from "@clack/prompts";
941
- import pc4 from "picocolors";
1533
+ import { defineCommand as defineCommand6 } from "citty";
1534
+ import * as p6 from "@clack/prompts";
1535
+ import pc5 from "picocolors";
942
1536
  var CLIENT_DISPLAY2 = {
943
1537
  "claude-desktop": "Claude",
944
1538
  cursor: "Cursor",
@@ -948,7 +1542,7 @@ var CLIENT_DISPLAY2 = {
948
1542
  function clientDisplayName(type) {
949
1543
  return CLIENT_DISPLAY2[type] ?? type;
950
1544
  }
951
- var remove_default = defineCommand5({
1545
+ var remove_default = defineCommand6({
952
1546
  meta: {
953
1547
  name: "remove",
954
1548
  description: "Remove an MCP server from one or more AI clients"
@@ -975,17 +1569,17 @@ var remove_default = defineCommand5({
975
1569
  }
976
1570
  },
977
1571
  async run({ args }) {
978
- p4.intro(pc4.bold("mcpman remove"));
1572
+ p6.intro(pc5.bold("mcpman remove"));
979
1573
  const serverName = args.server;
980
1574
  const servers = await getInstalledServers();
981
1575
  const match = servers.find((s) => s.name === serverName);
982
1576
  if (!match) {
983
- p4.log.warn(`Server "${serverName}" is not installed.`);
1577
+ p6.log.warn(`Server "${serverName}" is not installed.`);
984
1578
  const similar = servers.filter((s) => s.name.includes(serverName) || serverName.includes(s.name));
985
1579
  if (similar.length > 0) {
986
- p4.log.info(`Did you mean: ${similar.map((s) => pc4.cyan(s.name)).join(", ")}?`);
1580
+ p6.log.info(`Did you mean: ${similar.map((s) => pc5.cyan(s.name)).join(", ")}?`);
987
1581
  }
988
- p4.outro("Nothing to remove.");
1582
+ p6.outro("Nothing to remove.");
989
1583
  return;
990
1584
  }
991
1585
  let targetClients;
@@ -993,15 +1587,15 @@ var remove_default = defineCommand5({
993
1587
  targetClients = match.clients;
994
1588
  } else if (args.client) {
995
1589
  if (!match.clients.includes(args.client)) {
996
- p4.log.warn(`Server "${serverName}" is not installed in client "${args.client}".`);
997
- p4.outro("Nothing to remove.");
1590
+ p6.log.warn(`Server "${serverName}" is not installed in client "${args.client}".`);
1591
+ p6.outro("Nothing to remove.");
998
1592
  return;
999
1593
  }
1000
1594
  targetClients = [args.client];
1001
1595
  } else if (match.clients.length === 1) {
1002
1596
  targetClients = match.clients;
1003
1597
  } else {
1004
- const selected = await p4.multiselect({
1598
+ const selected = await p6.multiselect({
1005
1599
  message: `Remove "${serverName}" from which clients?`,
1006
1600
  options: match.clients.map((c) => ({
1007
1601
  value: c,
@@ -1009,19 +1603,19 @@ var remove_default = defineCommand5({
1009
1603
  })),
1010
1604
  required: true
1011
1605
  });
1012
- if (p4.isCancel(selected)) {
1013
- p4.outro("Cancelled.");
1606
+ if (p6.isCancel(selected)) {
1607
+ p6.outro("Cancelled.");
1014
1608
  process.exit(0);
1015
1609
  }
1016
1610
  targetClients = selected;
1017
1611
  }
1018
1612
  if (!args.yes) {
1019
1613
  const clientNames = targetClients.map(clientDisplayName).join(", ");
1020
- const confirmed = await p4.confirm({
1021
- message: `Remove ${pc4.cyan(serverName)} from ${pc4.yellow(clientNames)}?`
1614
+ const confirmed = await p6.confirm({
1615
+ message: `Remove ${pc5.cyan(serverName)} from ${pc5.yellow(clientNames)}?`
1022
1616
  });
1023
- if (p4.isCancel(confirmed) || !confirmed) {
1024
- p4.outro("Cancelled.");
1617
+ if (p6.isCancel(confirmed) || !confirmed) {
1618
+ p6.outro("Cancelled.");
1025
1619
  return;
1026
1620
  }
1027
1621
  }
@@ -1035,24 +1629,597 @@ var remove_default = defineCommand5({
1035
1629
  }
1036
1630
  try {
1037
1631
  await handler.removeServer(serverName);
1038
- p4.log.success(`Removed from ${clientDisplayName(clientType)}`);
1632
+ p6.log.success(`Removed from ${clientDisplayName(clientType)}`);
1039
1633
  } catch (err) {
1040
1634
  const msg = err instanceof Error ? err.message : String(err);
1041
1635
  errors.push(`${clientDisplayName(clientType)}: ${msg}`);
1042
1636
  }
1043
1637
  }
1044
1638
  if (errors.length > 0) {
1045
- for (const e of errors) p4.log.error(e);
1046
- p4.outro(pc4.red("Completed with errors."));
1639
+ for (const e of errors) p6.log.error(e);
1640
+ p6.outro(pc5.red("Completed with errors."));
1641
+ process.exit(1);
1642
+ }
1643
+ p6.outro(pc5.green(`Removed "${serverName}" successfully.`));
1644
+ }
1645
+ });
1646
+
1647
+ // src/commands/secrets.ts
1648
+ import { defineCommand as defineCommand7 } from "citty";
1649
+ import pc6 from "picocolors";
1650
+ import * as p7 from "@clack/prompts";
1651
+ function maskValue(value) {
1652
+ if (value.length <= 8) return "***";
1653
+ return `${value.slice(0, 4)}***${value.slice(-3)}`;
1654
+ }
1655
+ function parseKeyValue(input) {
1656
+ const idx = input.indexOf("=");
1657
+ if (idx <= 0) return null;
1658
+ return { key: input.slice(0, idx), value: input.slice(idx + 1) };
1659
+ }
1660
+ var setCommand = defineCommand7({
1661
+ meta: { name: "set", description: "Store an encrypted secret for a server" },
1662
+ args: {
1663
+ server: {
1664
+ type: "positional",
1665
+ description: "Server name (e.g. @modelcontextprotocol/server-github)",
1666
+ required: true
1667
+ },
1668
+ keyvalue: {
1669
+ type: "positional",
1670
+ description: "KEY=VALUE pair to store",
1671
+ required: true
1672
+ }
1673
+ },
1674
+ async run({ args }) {
1675
+ const parsed = parseKeyValue(args.keyvalue);
1676
+ if (!parsed) {
1677
+ console.error(pc6.red("\u2717") + " Invalid format. Expected KEY=VALUE");
1678
+ process.exit(1);
1679
+ }
1680
+ p7.intro(pc6.cyan("mcpman secrets set"));
1681
+ const isNew = listSecrets(args.server).length === 0 || !listSecrets(args.server)[0]?.keys.includes(parsed.key);
1682
+ const vaultPath = (await import("./vault-service-UTZAV6N6.js")).getVaultPath();
1683
+ const vaultExists = (await import("fs")).existsSync(vaultPath);
1684
+ const password = await getMasterPassword(!vaultExists && isNew);
1685
+ const spin = p7.spinner();
1686
+ spin.start("Encrypting secret...");
1687
+ try {
1688
+ setSecret(args.server, parsed.key, parsed.value, password);
1689
+ spin.stop(
1690
+ `${pc6.green("\u2713")} Stored ${pc6.bold(parsed.key)} for ${pc6.cyan(args.server)}`
1691
+ );
1692
+ } catch (err) {
1693
+ spin.stop(pc6.red("\u2717") + " Failed to store secret");
1694
+ console.error(pc6.dim(String(err)));
1695
+ process.exit(1);
1696
+ }
1697
+ p7.outro(pc6.dim("Secret encrypted and saved to vault."));
1698
+ }
1699
+ });
1700
+ var listCommand = defineCommand7({
1701
+ meta: { name: "list", description: "List secret keys stored in the vault" },
1702
+ args: {
1703
+ server: {
1704
+ type: "positional",
1705
+ description: "Filter by server name (optional)",
1706
+ required: false
1707
+ }
1708
+ },
1709
+ async run({ args }) {
1710
+ const results = listSecrets(args.server || void 0);
1711
+ if (results.length === 0) {
1712
+ const filter = args.server ? ` for ${pc6.cyan(args.server)}` : "";
1713
+ console.log(pc6.dim(`No secrets stored${filter}.`));
1714
+ return;
1715
+ }
1716
+ console.log("");
1717
+ for (const { server, keys } of results) {
1718
+ console.log(pc6.bold(pc6.cyan(server)));
1719
+ for (const key of keys) {
1720
+ console.log(` ${pc6.green("\u25CF")} ${pc6.bold(key)} ${pc6.dim(maskValue("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"))}`);
1721
+ }
1722
+ console.log("");
1723
+ }
1724
+ const total = results.reduce((n, r) => n + r.keys.length, 0);
1725
+ console.log(pc6.dim(` ${total} secret${total !== 1 ? "s" : ""} in ${results.length} server${results.length !== 1 ? "s" : ""}`));
1726
+ }
1727
+ });
1728
+ var removeCommand = defineCommand7({
1729
+ meta: { name: "remove", description: "Delete a secret from the vault" },
1730
+ args: {
1731
+ server: {
1732
+ type: "positional",
1733
+ description: "Server name",
1734
+ required: true
1735
+ },
1736
+ key: {
1737
+ type: "positional",
1738
+ description: "Secret key to remove",
1739
+ required: true
1740
+ }
1741
+ },
1742
+ async run({ args }) {
1743
+ const confirmed = await p7.confirm({
1744
+ message: `Remove ${pc6.bold(args.key)} from ${pc6.cyan(args.server)}?`,
1745
+ initialValue: false
1746
+ });
1747
+ if (p7.isCancel(confirmed) || !confirmed) {
1748
+ p7.cancel("Cancelled.");
1749
+ return;
1750
+ }
1751
+ try {
1752
+ removeSecret(args.server, args.key);
1753
+ console.log(`${pc6.green("\u2713")} Removed ${pc6.bold(args.key)} from ${pc6.cyan(args.server)}`);
1754
+ } catch (err) {
1755
+ console.error(pc6.red("\u2717") + " Failed to remove secret");
1756
+ console.error(pc6.dim(String(err)));
1757
+ process.exit(1);
1758
+ }
1759
+ }
1760
+ });
1761
+ var secrets_default = defineCommand7({
1762
+ meta: {
1763
+ name: "secrets",
1764
+ description: "Manage encrypted secrets for MCP servers"
1765
+ },
1766
+ subCommands: {
1767
+ set: setCommand,
1768
+ list: listCommand,
1769
+ remove: removeCommand
1770
+ }
1771
+ });
1772
+
1773
+ // src/commands/sync.ts
1774
+ import { defineCommand as defineCommand8 } from "citty";
1775
+ import * as p8 from "@clack/prompts";
1776
+ import pc7 from "picocolors";
1777
+
1778
+ // src/core/config-diff.ts
1779
+ function reconstructServerEntry(lockEntry) {
1780
+ const entry = {
1781
+ command: lockEntry.command
1782
+ };
1783
+ if (lockEntry.args && lockEntry.args.length > 0) {
1784
+ entry.args = lockEntry.args;
1785
+ }
1786
+ if (lockEntry.envVars && lockEntry.envVars.length > 0) {
1787
+ entry.env = Object.fromEntries(lockEntry.envVars.map((k) => [k, ""]));
1788
+ }
1789
+ return entry;
1790
+ }
1791
+ function computeDiff(lockfile, clientConfigs, options = {}) {
1792
+ const actions = [];
1793
+ for (const [server, lockEntry] of Object.entries(lockfile.servers)) {
1794
+ for (const client of lockEntry.clients) {
1795
+ const config = clientConfigs.get(client);
1796
+ if (!config) continue;
1797
+ if (server in config.servers) {
1798
+ actions.push({ server, client, action: "ok" });
1799
+ } else {
1800
+ actions.push({
1801
+ server,
1802
+ client,
1803
+ action: "add",
1804
+ entry: reconstructServerEntry(lockEntry)
1805
+ });
1806
+ }
1807
+ }
1808
+ }
1809
+ const extraAction = options.remove ? "remove" : "extra";
1810
+ for (const [client, config] of clientConfigs) {
1811
+ for (const server of Object.keys(config.servers)) {
1812
+ if (!(server in lockfile.servers)) {
1813
+ actions.push({ server, client, action: extraAction });
1814
+ }
1815
+ }
1816
+ }
1817
+ return actions;
1818
+ }
1819
+ function computeDiffFromClient(sourceClient, clientConfigs, options = {}) {
1820
+ const actions = [];
1821
+ const sourceConfig = clientConfigs.get(sourceClient);
1822
+ if (!sourceConfig) return [];
1823
+ const extraAction = options.remove ? "remove" : "extra";
1824
+ for (const [client, config] of clientConfigs) {
1825
+ if (client === sourceClient) continue;
1826
+ for (const [server, entry] of Object.entries(sourceConfig.servers)) {
1827
+ if (server in config.servers) {
1828
+ actions.push({ server, client, action: "ok" });
1829
+ } else {
1830
+ actions.push({ server, client, action: "add", entry });
1831
+ }
1832
+ }
1833
+ for (const server of Object.keys(config.servers)) {
1834
+ if (!(server in sourceConfig.servers)) {
1835
+ actions.push({ server, client, action: extraAction });
1836
+ }
1837
+ }
1838
+ }
1839
+ return actions;
1840
+ }
1841
+
1842
+ // src/core/sync-engine.ts
1843
+ async function applySyncActions(actions, clients) {
1844
+ const result = { applied: 0, removed: 0, failed: 0, errors: [] };
1845
+ const addActions = actions.filter((a) => a.action === "add" && a.entry);
1846
+ for (const action of addActions) {
1847
+ const handler = clients.get(action.client);
1848
+ if (!handler || !action.entry) {
1849
+ result.failed++;
1850
+ result.errors.push({
1851
+ server: action.server,
1852
+ client: action.client,
1853
+ error: "No handler available for client"
1854
+ });
1855
+ continue;
1856
+ }
1857
+ try {
1858
+ await handler.addServer(action.server, action.entry);
1859
+ result.applied++;
1860
+ } catch (err) {
1861
+ result.failed++;
1862
+ result.errors.push({
1863
+ server: action.server,
1864
+ client: action.client,
1865
+ error: String(err)
1866
+ });
1867
+ }
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
+ }
1893
+ return result;
1894
+ }
1895
+ async function getClientConfigs() {
1896
+ const configs = /* @__PURE__ */ new Map();
1897
+ const handlers = /* @__PURE__ */ new Map();
1898
+ let installedClients;
1899
+ try {
1900
+ installedClients = await getInstalledClients();
1901
+ } catch {
1902
+ return { configs, handlers };
1903
+ }
1904
+ await Promise.all(
1905
+ installedClients.map(async (handler) => {
1906
+ try {
1907
+ const config = await handler.readConfig();
1908
+ configs.set(handler.type, config);
1909
+ handlers.set(handler.type, handler);
1910
+ } catch (err) {
1911
+ console.warn(`[mcpman] Warning: could not read config for ${handler.displayName}: ${String(err)}`);
1912
+ }
1913
+ })
1914
+ );
1915
+ return { configs, handlers };
1916
+ }
1917
+
1918
+ // src/commands/sync.ts
1919
+ var VALID_CLIENTS = ["claude-desktop", "cursor", "vscode", "windsurf"];
1920
+ var CLIENT_DISPLAY3 = {
1921
+ "claude-desktop": "Claude Desktop",
1922
+ cursor: "Cursor",
1923
+ vscode: "VS Code",
1924
+ windsurf: "Windsurf"
1925
+ };
1926
+ var sync_default = defineCommand8({
1927
+ meta: {
1928
+ name: "sync",
1929
+ description: "Sync MCP server configs across all detected AI clients"
1930
+ },
1931
+ args: {
1932
+ "dry-run": {
1933
+ type: "boolean",
1934
+ description: "Preview changes without applying them",
1935
+ default: false
1936
+ },
1937
+ remove: {
1938
+ type: "boolean",
1939
+ description: "Remove extra servers not in lockfile",
1940
+ default: false
1941
+ },
1942
+ source: {
1943
+ type: "string",
1944
+ description: "Use a specific client as source of truth (claude-desktop, cursor, vscode, windsurf)"
1945
+ },
1946
+ yes: {
1947
+ type: "boolean",
1948
+ description: "Skip confirmation prompt",
1949
+ default: false
1950
+ }
1951
+ },
1952
+ async run({ args }) {
1953
+ p8.intro(`${pc7.cyan("mcpman sync")}`);
1954
+ const sourceClient = args.source;
1955
+ if (sourceClient && !VALID_CLIENTS.includes(sourceClient)) {
1956
+ p8.log.error(`Invalid --source "${sourceClient}". Must be one of: ${VALID_CLIENTS.join(", ")}`);
1957
+ process.exit(1);
1958
+ }
1959
+ const spinner5 = p8.spinner();
1960
+ spinner5.start("Detecting clients and reading configs...");
1961
+ const { configs, handlers } = await getClientConfigs();
1962
+ spinner5.stop(`Found ${configs.size} client(s)`);
1963
+ if (configs.size === 0) {
1964
+ p8.log.warn("No AI clients detected. Install Claude Desktop, Cursor, VS Code, or Windsurf first.");
1965
+ process.exit(0);
1966
+ }
1967
+ const diffOptions = { remove: args.remove };
1968
+ let actions;
1969
+ if (sourceClient) {
1970
+ if (!configs.has(sourceClient)) {
1971
+ p8.log.error(`Source client "${sourceClient}" is not detected or its config is unreadable.`);
1972
+ process.exit(1);
1973
+ }
1974
+ p8.log.info(`Using ${CLIENT_DISPLAY3[sourceClient]} as source of truth`);
1975
+ actions = computeDiffFromClient(sourceClient, configs, diffOptions);
1976
+ } else {
1977
+ const lockfile = readLockfile();
1978
+ actions = computeDiff(lockfile, configs, diffOptions);
1979
+ }
1980
+ printDiffTable(actions);
1981
+ const addCount = actions.filter((a) => a.action === "add").length;
1982
+ const extraCount = actions.filter((a) => a.action === "extra").length;
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."));
1986
+ process.exit(0);
1987
+ }
1988
+ const parts = [];
1989
+ if (addCount > 0) parts.push(pc7.green(`${addCount} to add`));
1990
+ if (removeCount > 0) parts.push(pc7.red(`${removeCount} to remove`));
1991
+ if (extraCount > 0) parts.push(pc7.yellow(`${extraCount} extra (informational)`));
1992
+ p8.log.info(parts.join(" \xB7 "));
1993
+ if (args["dry-run"]) {
1994
+ p8.outro(pc7.dim("Dry run \u2014 no changes applied."));
1995
+ process.exit(1);
1996
+ }
1997
+ if (addCount === 0 && removeCount === 0) {
1998
+ p8.outro(pc7.dim("No additions needed. Extra servers left untouched."));
1999
+ process.exit(1);
2000
+ }
2001
+ if (!args.yes) {
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?`,
2007
+ initialValue: true
2008
+ });
2009
+ if (p8.isCancel(confirmed) || !confirmed) {
2010
+ p8.outro(pc7.dim("Cancelled \u2014 no changes applied."));
2011
+ process.exit(0);
2012
+ }
2013
+ }
2014
+ spinner5.start("Applying sync changes...");
2015
+ const result = await applySyncActions(actions, handlers);
2016
+ spinner5.stop("Done");
2017
+ if (result.applied > 0) {
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.`);
2022
+ }
2023
+ if (result.failed > 0) {
2024
+ for (const e of result.errors) {
2025
+ p8.log.error(`Failed to sync "${e.server}" on ${e.client}: ${e.error}`);
2026
+ }
2027
+ }
2028
+ p8.outro(result.failed === 0 ? pc7.green("Sync complete.") : pc7.yellow("Sync complete with errors."));
2029
+ process.exit(result.failed > 0 ? 1 : 0);
2030
+ }
2031
+ });
2032
+ function printDiffTable(actions) {
2033
+ if (actions.length === 0) {
2034
+ p8.log.info("No actions to display.");
2035
+ return;
2036
+ }
2037
+ const nameWidth = Math.max(6, ...actions.map((a) => a.server.length));
2038
+ const clientWidth = Math.max(6, ...actions.map((a) => CLIENT_DISPLAY3[a.client]?.length ?? a.client.length));
2039
+ const header = ` ${pad2("SERVER", nameWidth)} ${pad2("CLIENT", clientWidth)} STATUS`;
2040
+ console.log(pc7.dim(header));
2041
+ console.log(pc7.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientWidth)} ------`));
2042
+ for (const action of actions) {
2043
+ const clientDisplay = CLIENT_DISPLAY3[action.client] ?? action.client;
2044
+ const [icon, statusText] = formatAction(action.action);
2045
+ console.log(` ${pad2(action.server, nameWidth)} ${pad2(clientDisplay, clientWidth)} ${icon} ${statusText}`);
2046
+ }
2047
+ console.log("");
2048
+ }
2049
+ function formatAction(action) {
2050
+ switch (action) {
2051
+ case "add":
2052
+ return [pc7.green("+"), pc7.green("missing \u2014 will add")];
2053
+ case "extra":
2054
+ return [pc7.yellow("?"), pc7.yellow("extra (not in lockfile)")];
2055
+ case "remove":
2056
+ return [pc7.red("\u2013"), pc7.red("extra \u2014 will remove")];
2057
+ case "ok":
2058
+ return [pc7.dim("\xB7"), pc7.dim("in sync")];
2059
+ }
2060
+ }
2061
+ function pad2(s, width) {
2062
+ return s.length >= width ? s : s + " ".repeat(width - s.length);
2063
+ }
2064
+
2065
+ // src/commands/update.ts
2066
+ import { defineCommand as defineCommand9 } from "citty";
2067
+ import * as p9 from "@clack/prompts";
2068
+ import pc9 from "picocolors";
2069
+
2070
+ // src/core/update-notifier.ts
2071
+ import fs3 from "fs";
2072
+ import path4 from "path";
2073
+ import os3 from "os";
2074
+ import pc8 from "picocolors";
2075
+ var CACHE_FILE = path4.join(os3.homedir(), ".mcpman", ".update-check");
2076
+ var TTL_MS = 24 * 60 * 60 * 1e3;
2077
+ function writeUpdateCache(data) {
2078
+ try {
2079
+ const dir = path4.dirname(CACHE_FILE);
2080
+ if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
2081
+ const tmp = `${CACHE_FILE}.tmp`;
2082
+ fs3.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
2083
+ fs3.renameSync(tmp, CACHE_FILE);
2084
+ } catch {
2085
+ }
2086
+ }
2087
+
2088
+ // src/commands/update.ts
2089
+ async function loadClients3() {
2090
+ try {
2091
+ const mod = await import("./client-detector-SUIJSIYM.js");
2092
+ return mod.getInstalledClients();
2093
+ } catch {
2094
+ return [];
2095
+ }
2096
+ }
2097
+ function printTable(updates) {
2098
+ const NAME_W = 28;
2099
+ const VER_W = 10;
2100
+ const header = [
2101
+ "NAME".padEnd(NAME_W),
2102
+ "CURRENT".padEnd(VER_W),
2103
+ "LATEST".padEnd(VER_W),
2104
+ "STATUS"
2105
+ ].join(" ");
2106
+ console.log(pc9.bold(`
2107
+ ${header}`));
2108
+ console.log(pc9.dim(` ${"\u2500".repeat(NAME_W + VER_W * 2 + 20)}`));
2109
+ for (const u of updates) {
2110
+ const nameCol = u.server.slice(0, NAME_W).padEnd(NAME_W);
2111
+ const curCol = u.currentVersion.padEnd(VER_W);
2112
+ const latCol = u.latestVersion.padEnd(VER_W);
2113
+ const statusCol = u.hasUpdate ? pc9.yellow(`Update available${u.updateType ? ` [${u.updateType}]` : ""}`) : pc9.green("Up to date");
2114
+ console.log(` ${nameCol} ${curCol} ${latCol} ${statusCol}`);
2115
+ }
2116
+ console.log();
2117
+ }
2118
+ var update_default = defineCommand9({
2119
+ meta: {
2120
+ name: "update",
2121
+ description: "Check for and apply updates to installed MCP servers"
2122
+ },
2123
+ args: {
2124
+ server: {
2125
+ type: "positional",
2126
+ description: "Server name to update (omit to update all)",
2127
+ required: false
2128
+ },
2129
+ check: {
2130
+ type: "boolean",
2131
+ description: "Check only \u2014 do not apply updates",
2132
+ default: false
2133
+ },
2134
+ yes: {
2135
+ type: "boolean",
2136
+ description: "Skip confirmation prompt",
2137
+ default: false
2138
+ },
2139
+ json: {
2140
+ type: "boolean",
2141
+ description: "Output results as JSON",
2142
+ default: false
2143
+ }
2144
+ },
2145
+ async run({ args }) {
2146
+ const lockfile = readLockfile();
2147
+ const servers = lockfile.servers;
2148
+ const targetEntries = args.server ? Object.entries(servers).filter(([name]) => name === args.server) : Object.entries(servers);
2149
+ if (targetEntries.length === 0) {
2150
+ if (args.server) {
2151
+ console.error(`Server '${args.server}' not found in lockfile.`);
2152
+ } else {
2153
+ console.log("No servers installed. Run mcpman install <server> first.");
2154
+ }
2155
+ process.exit(1);
2156
+ }
2157
+ const spinner5 = p9.spinner();
2158
+ spinner5.start("Checking versions...");
2159
+ let updates;
2160
+ try {
2161
+ const partialLock = {
2162
+ lockfileVersion: 1,
2163
+ servers: Object.fromEntries(targetEntries)
2164
+ };
2165
+ updates = await checkAllVersions(partialLock);
2166
+ } catch (err) {
2167
+ spinner5.stop("Version check failed");
2168
+ console.error(err instanceof Error ? err.message : String(err));
1047
2169
  process.exit(1);
1048
2170
  }
1049
- p4.outro(pc4.green(`Removed "${serverName}" successfully.`));
2171
+ spinner5.stop(`Checked ${updates.length} server(s)`);
2172
+ if (args.json) {
2173
+ console.log(JSON.stringify(updates, null, 2));
2174
+ return;
2175
+ }
2176
+ printTable(updates);
2177
+ const outdated = updates.filter((u) => u.hasUpdate);
2178
+ if (outdated.length === 0) {
2179
+ console.log(pc9.green(" All servers are up to date."));
2180
+ return;
2181
+ }
2182
+ if (args.check) {
2183
+ console.log(pc9.yellow(` ${outdated.length} update(s) available. Run mcpman update to apply.`));
2184
+ return;
2185
+ }
2186
+ if (!args.yes) {
2187
+ const confirmed = await p9.confirm({
2188
+ message: `Apply ${outdated.length} update(s)?`,
2189
+ initialValue: true
2190
+ });
2191
+ if (p9.isCancel(confirmed) || !confirmed) {
2192
+ p9.outro("Cancelled.");
2193
+ return;
2194
+ }
2195
+ }
2196
+ const clients = await loadClients3();
2197
+ let successCount = 0;
2198
+ for (const update of outdated) {
2199
+ const s = p9.spinner();
2200
+ s.start(`Updating ${update.server}...`);
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}`);
2208
+ successCount++;
2209
+ } else {
2210
+ s.stop(`${pc9.red("\u2717")} ${update.server}: ${result.error}`);
2211
+ }
2212
+ }
2213
+ const freshLockfile = readLockfile(resolveLockfilePath());
2214
+ const freshUpdates = await checkAllVersions(freshLockfile);
2215
+ writeUpdateCache({ lastCheck: (/* @__PURE__ */ new Date()).toISOString(), updates: freshUpdates });
2216
+ p9.outro(`${successCount} of ${outdated.length} server(s) updated.`);
1050
2217
  }
1051
2218
  });
1052
2219
 
1053
2220
  // src/utils/constants.ts
1054
2221
  var APP_NAME = "mcpman";
1055
- var APP_VERSION = "0.1.1";
2222
+ var APP_VERSION = "0.3.0";
1056
2223
  var APP_DESCRIPTION = "The package manager for MCP servers";
1057
2224
 
1058
2225
  // src/index.ts
@@ -1060,7 +2227,7 @@ process.on("SIGINT", () => {
1060
2227
  console.log("\nAborted.");
1061
2228
  process.exit(130);
1062
2229
  });
1063
- var main = defineCommand6({
2230
+ var main = defineCommand10({
1064
2231
  meta: {
1065
2232
  name: APP_NAME,
1066
2233
  version: APP_VERSION,
@@ -1071,7 +2238,11 @@ var main = defineCommand6({
1071
2238
  list: list_default,
1072
2239
  remove: remove_default,
1073
2240
  doctor: doctor_default,
1074
- init: init_default
2241
+ init: init_default,
2242
+ secrets: secrets_default,
2243
+ sync: sync_default,
2244
+ audit: audit_default,
2245
+ update: update_default
1075
2246
  }
1076
2247
  });
1077
2248
  runMain(main);