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/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 p8 = scanServer(name, entry).then((r) => {
214
+ const p10 = scanServer(name, entry).then((r) => {
213
215
  results.push(r);
214
- executing.delete(p8);
216
+ executing.delete(p10);
215
217
  });
216
- executing.add(p8);
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: "Show available fix versions for vulnerable packages",
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
- const withVulns = reports.filter((r) => r.vulnerabilities.length > 0);
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
- const version = typeof data.version === "string" ? data.version : "latest";
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 resolveFromGitHub(githubUrl) {
794
- const match = githubUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
795
- if (!match) {
796
- throw new Error(`Invalid GitHub URL: ${githubUrl}`);
797
- }
798
- const [, owner, repo] = match;
799
- const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/package.json`;
800
- let pkgData = {};
801
- try {
802
- const res = await fetch(rawUrl, { signal: AbortSignal.timeout(8e3) });
803
- if (res.ok) {
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
- const version = typeof pkgData.version === "string" ? pkgData.version : "main";
809
- const name = typeof pkgData.name === "string" ? pkgData.name : `${owner}/${repo}`;
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
- p.intro("mcpman init");
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
- p.log.warn(`Lockfile already exists: ${existing} \u2014 overwriting (non-interactive).`);
1140
+ p2.log.warn(`Lockfile already exists: ${existing} \u2014 overwriting (non-interactive).`);
844
1141
  } else {
845
- p.log.warn(`Lockfile already exists: ${existing}`);
846
- const overwrite = await p.confirm({ message: "Overwrite?" });
847
- if (p.isCancel(overwrite) || !overwrite) {
848
- 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.");
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
- 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.");
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
- p.log.info("No existing servers found in any client config.");
873
- 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!`);
874
1171
  return;
875
1172
  }
876
1173
  let selected;
877
1174
  if (nonInteractive) {
878
1175
  selected = clientServers.map((cs) => cs.client.type);
879
- 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).`);
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 p.multiselect({
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 (p.isCancel(toImport)) {
891
- p.outro(`Created empty ${LOCKFILE_NAME}`);
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
- p.outro(
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 p2 from "@clack/prompts";
1224
+ import * as p4 from "@clack/prompts";
928
1225
 
929
- // src/core/server-resolver.ts
930
- function detectSource(input) {
931
- if (input.startsWith("smithery:")) {
932
- return { type: "smithery", input: input.slice(9) };
933
- }
934
- if (input.startsWith("https://github.com/") || input.startsWith("github.com/")) {
935
- return { type: "github", input };
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 resolveServer(input) {
952
- const source = detectSource(input);
953
- switch (source.type) {
954
- case "smithery":
955
- return resolveFromSmithery(source.input);
956
- case "github":
957
- return resolveFromGitHub(source.input);
958
- case "npm":
959
- 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)}`);
960
1255
  }
961
1256
  }
962
1257
 
963
1258
  // src/core/installer.ts
964
- async function loadClients() {
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
- p2.intro("mcpman install");
974
- const spinner5 = p2.spinner();
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
- p2.log.error(err instanceof Error ? err.message : String(err));
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 loadClients();
1280
+ const clients = await loadClients2();
986
1281
  if (clients.length === 0) {
987
- p2.log.warn("No supported AI clients detected on this machine.");
988
- 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");
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
- p2.log.error(`Client '${options.client}' not found or not installed.`);
996
- 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(", ")}`);
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 p2.multiselect({
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 (p2.isCancel(chosen)) {
1009
- p2.outro("Cancelled.");
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 collectedEnv = { ...providedEnv };
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 p2.text({
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 (p2.isCancel(val)) {
1028
- p2.outro("Cancelled.");
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
- p2.log.warn(`Failed to write to ${client.displayName}: ${err instanceof Error ? err.message : String(err)}`);
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
- p2.log.success(`Lockfile updated: ${lockPath}`);
1066
- 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(", ")}`);
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 p3 from "@clack/prompts";
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
- p3.intro(`mcpman install (restore from ${lockPath})`);
1141
- 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)...`);
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
- p3.outro("Restore complete.");
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 p4 from "@clack/prompts";
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
- p4.intro(pc5.bold("mcpman remove"));
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
- p4.log.warn(`Server "${serverName}" is not installed.`);
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
- p4.log.info(`Did you mean: ${similar.map((s) => pc5.cyan(s.name)).join(", ")}?`);
1580
+ p6.log.info(`Did you mean: ${similar.map((s) => pc5.cyan(s.name)).join(", ")}?`);
1282
1581
  }
1283
- p4.outro("Nothing to remove.");
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
- p4.log.warn(`Server "${serverName}" is not installed in client "${args.client}".`);
1292
- p4.outro("Nothing to remove.");
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 p4.multiselect({
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 (p4.isCancel(selected)) {
1308
- p4.outro("Cancelled.");
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 p4.confirm({
1614
+ const confirmed = await p6.confirm({
1316
1615
  message: `Remove ${pc5.cyan(serverName)} from ${pc5.yellow(clientNames)}?`
1317
1616
  });
1318
- if (p4.isCancel(confirmed) || !confirmed) {
1319
- p4.outro("Cancelled.");
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
- p4.log.success(`Removed from ${clientDisplayName(clientType)}`);
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) p4.log.error(e);
1341
- p4.outro(pc5.red("Completed with errors."));
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
- p4.outro(pc5.green(`Removed "${serverName}" successfully.`));
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 p5 from "@clack/prompts";
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
- p5.intro(pc6.cyan("mcpman secrets set"));
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 = p5.spinner();
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
- p5.outro(pc6.dim("Secret encrypted and saved to vault."));
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 p5.confirm({
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 (p5.isCancel(confirmed) || !confirmed) {
1449
- p5.cancel("Cancelled.");
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 p6 from "@clack/prompts";
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: "extra" });
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: "extra" });
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
- p6.intro(`${pc7.cyan("mcpman sync")}`);
1953
+ p8.intro(`${pc7.cyan("mcpman sync")}`);
1624
1954
  const sourceClient = args.source;
1625
1955
  if (sourceClient && !VALID_CLIENTS.includes(sourceClient)) {
1626
- p6.log.error(`Invalid --source "${sourceClient}". Must be one of: ${VALID_CLIENTS.join(", ")}`);
1956
+ p8.log.error(`Invalid --source "${sourceClient}". Must be one of: ${VALID_CLIENTS.join(", ")}`);
1627
1957
  process.exit(1);
1628
1958
  }
1629
- const spinner5 = p6.spinner();
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
- p6.log.warn("No AI clients detected. Install Claude Desktop, Cursor, VS Code, or Windsurf first.");
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
- p6.log.error(`Source client "${sourceClient}" is not detected or its config is unreadable.`);
1971
+ p8.log.error(`Source client "${sourceClient}" is not detected or its config is unreadable.`);
1641
1972
  process.exit(1);
1642
1973
  }
1643
- p6.log.info(`Using ${CLIENT_DISPLAY3[sourceClient]} as source of truth`);
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
- if (addCount === 0 && extraCount === 0) {
1653
- p6.outro(pc7.green("All clients are in sync."));
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
- p6.log.info(parts.join(" \xB7 "));
1992
+ p8.log.info(parts.join(" \xB7 "));
1660
1993
  if (args["dry-run"]) {
1661
- p6.outro(pc7.dim("Dry run \u2014 no changes applied."));
1994
+ p8.outro(pc7.dim("Dry run \u2014 no changes applied."));
1662
1995
  process.exit(1);
1663
1996
  }
1664
- if (addCount === 0) {
1665
- p6.outro(pc7.dim("No additions needed. Extra servers left untouched."));
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 confirmed = await p6.confirm({
1670
- message: `Apply ${addCount} addition(s) to client configs?`,
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 (p6.isCancel(confirmed) || !confirmed) {
1674
- p6.outro(pc7.dim("Cancelled \u2014 no changes applied."));
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
- p6.log.success(`Added ${result.applied} server(s) to client configs.`);
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
- p6.log.error(`Failed to add "${e.server}" to ${e.client}: ${e.error}`);
2025
+ p8.log.error(`Failed to sync "${e.server}" on ${e.client}: ${e.error}`);
1687
2026
  }
1688
2027
  }
1689
- p6.outro(result.failed === 0 ? pc7.green("Sync complete.") : pc7.yellow("Sync complete with errors."));
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
- p6.log.info("No actions to display.");
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 p7 from "@clack/prompts";
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 loadClients2() {
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 = p7.spinner();
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 p7.confirm({
2187
+ const confirmed = await p9.confirm({
1967
2188
  message: `Apply ${outdated.length} update(s)?`,
1968
2189
  initialValue: true
1969
2190
  });
1970
- if (p7.isCancel(confirmed) || !confirmed) {
1971
- p7.outro("Cancelled.");
2191
+ if (p9.isCancel(confirmed) || !confirmed) {
2192
+ p9.outro("Cancelled.");
1972
2193
  return;
1973
2194
  }
1974
2195
  }
1975
- const clients = await loadClients2();
2196
+ const clients = await loadClients3();
1976
2197
  let successCount = 0;
1977
2198
  for (const update of outdated) {
1978
- const lockEntry = servers[update.server];
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
- try {
1983
- const metadata = await resolveServer(input);
1984
- const integrity = computeIntegrity(metadata.resolved);
1985
- addEntry(update.server, {
1986
- ...lockEntry,
1987
- version: metadata.version,
1988
- resolved: metadata.resolved,
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
- } catch (err) {
2009
- s.stop(`${pc9.red("\u2717")} ${update.server}: ${err instanceof Error ? err.message : String(err)}`);
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
- p7.outro(`${successCount} of ${outdated.length} server(s) updated.`);
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.2.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