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.cjs CHANGED
@@ -159,9 +159,9 @@ async function atomicWrite(filePath, content) {
159
159
  throw err;
160
160
  }
161
161
  }
162
- async function pathExists(p9) {
162
+ async function pathExists(p11) {
163
163
  try {
164
- await import_node_fs3.default.promises.access(p9);
164
+ await import_node_fs3.default.promises.access(p11);
165
165
  return true;
166
166
  } catch {
167
167
  return false;
@@ -482,20 +482,20 @@ function decrypt(entry, password2) {
482
482
  ]);
483
483
  return decrypted.toString("utf-8");
484
484
  }
485
- async function getMasterPassword(confirm6 = false) {
485
+ async function getMasterPassword(confirm8 = false) {
486
486
  if (_cachedPassword) return _cachedPassword;
487
- const password2 = await p5.password({
487
+ const password2 = await p3.password({
488
488
  message: "Enter vault master password:",
489
489
  validate: (v) => v.length < 8 ? "Password must be at least 8 characters" : void 0
490
490
  });
491
- if (p5.isCancel(password2)) {
492
- p5.cancel("Vault access cancelled.");
491
+ if (p3.isCancel(password2)) {
492
+ p3.cancel("Vault access cancelled.");
493
493
  process.exit(0);
494
494
  }
495
- if (confirm6) {
496
- const confirm22 = await p5.password({ message: "Confirm master password:" });
497
- if (p5.isCancel(confirm22) || confirm22 !== password2) {
498
- p5.cancel("Passwords do not match.");
495
+ if (confirm8) {
496
+ const confirm22 = await p3.password({ message: "Confirm master password:" });
497
+ if (p3.isCancel(confirm22) || confirm22 !== password2) {
498
+ p3.cancel("Passwords do not match.");
499
499
  process.exit(1);
500
500
  }
501
501
  }
@@ -545,7 +545,7 @@ function listSecrets(server, vaultPath = getVaultPath()) {
545
545
  keys: Object.keys(keys)
546
546
  }));
547
547
  }
548
- var import_node_crypto2, import_node_fs4, import_node_os4, import_node_path6, p5, _cachedPassword;
548
+ var import_node_crypto2, import_node_fs4, import_node_os4, import_node_path6, p3, _cachedPassword;
549
549
  var init_vault_service = __esm({
550
550
  "src/core/vault-service.ts"() {
551
551
  "use strict";
@@ -554,7 +554,7 @@ var init_vault_service = __esm({
554
554
  import_node_fs4 = __toESM(require("fs"), 1);
555
555
  import_node_os4 = __toESM(require("os"), 1);
556
556
  import_node_path6 = __toESM(require("path"), 1);
557
- p5 = __toESM(require("@clack/prompts"), 1);
557
+ p3 = __toESM(require("@clack/prompts"), 1);
558
558
  _cachedPassword = null;
559
559
  process.on("exit", () => {
560
560
  _cachedPassword = null;
@@ -569,6 +569,7 @@ var import_citty10 = require("citty");
569
569
  // src/commands/audit.ts
570
570
  init_cjs_shims();
571
571
  var import_citty = require("citty");
572
+ var p = __toESM(require("@clack/prompts"), 1);
572
573
  var import_picocolors = __toESM(require("picocolors"), 1);
573
574
  var import_nanospinner = require("nanospinner");
574
575
 
@@ -766,17 +767,332 @@ async function scanAllServers(servers, concurrency = 3) {
766
767
  const results = [];
767
768
  const executing = /* @__PURE__ */ new Set();
768
769
  for (const [name, entry] of entries) {
769
- const p9 = scanServer(name, entry).then((r) => {
770
+ const p11 = scanServer(name, entry).then((r) => {
770
771
  results.push(r);
771
- executing.delete(p9);
772
+ executing.delete(p11);
772
773
  });
773
- executing.add(p9);
774
+ executing.add(p11);
774
775
  if (executing.size >= concurrency) await Promise.race(executing);
775
776
  }
776
777
  await Promise.all(executing);
777
778
  return results;
778
779
  }
779
780
 
781
+ // src/core/version-checker.ts
782
+ init_cjs_shims();
783
+ function compareVersions(a, b) {
784
+ const aParts = a.replace(/^v/, "").split(".").map(Number);
785
+ const bParts = b.replace(/^v/, "").split(".").map(Number);
786
+ const len = Math.max(aParts.length, bParts.length);
787
+ for (let i = 0; i < len; i++) {
788
+ const aN = aParts[i] ?? 0;
789
+ const bN = bParts[i] ?? 0;
790
+ if (Number.isNaN(aN) || Number.isNaN(bN)) return 0;
791
+ if (aN < bN) return -1;
792
+ if (aN > bN) return 1;
793
+ }
794
+ return 0;
795
+ }
796
+ function detectUpdateType(current, latest) {
797
+ const cParts = current.replace(/^v/, "").split(".").map(Number);
798
+ const lParts = latest.replace(/^v/, "").split(".").map(Number);
799
+ if ((lParts[0] ?? 0) > (cParts[0] ?? 0)) return "major";
800
+ if ((lParts[1] ?? 0) > (cParts[1] ?? 0)) return "minor";
801
+ return "patch";
802
+ }
803
+ async function fetchNpmLatest(packageName) {
804
+ try {
805
+ const res = await fetch(
806
+ `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`,
807
+ {
808
+ headers: { Accept: "application/json" },
809
+ signal: AbortSignal.timeout(8e3)
810
+ }
811
+ );
812
+ if (!res.ok) return null;
813
+ const data = await res.json();
814
+ return typeof data.version === "string" ? data.version : null;
815
+ } catch {
816
+ return null;
817
+ }
818
+ }
819
+ async function fetchSmitheryLatest(name) {
820
+ try {
821
+ const res = await fetch(
822
+ `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`,
823
+ {
824
+ headers: { Accept: "application/json" },
825
+ signal: AbortSignal.timeout(8e3)
826
+ }
827
+ );
828
+ if (!res.ok) return null;
829
+ const data = await res.json();
830
+ return typeof data.version === "string" ? data.version : null;
831
+ } catch {
832
+ return null;
833
+ }
834
+ }
835
+ async function fetchGithubLatest(resolved) {
836
+ const match = resolved.match(/github\.com\/([^/]+)\/([^/]+)/);
837
+ if (!match) return null;
838
+ const [, owner, repo] = match;
839
+ try {
840
+ const res = await fetch(
841
+ `https://api.github.com/repos/${owner}/${repo}/releases/latest`,
842
+ {
843
+ headers: { Accept: "application/json" },
844
+ signal: AbortSignal.timeout(8e3)
845
+ }
846
+ );
847
+ if (!res.ok) return null;
848
+ const data = await res.json();
849
+ return typeof data.tag_name === "string" ? data.tag_name.replace(/^v/, "") : null;
850
+ } catch {
851
+ return null;
852
+ }
853
+ }
854
+ async function checkVersion(name, lockEntry) {
855
+ const current = lockEntry.version;
856
+ let latest = null;
857
+ if (lockEntry.source === "npm") {
858
+ latest = await fetchNpmLatest(name);
859
+ } else if (lockEntry.source === "smithery") {
860
+ latest = await fetchSmitheryLatest(name);
861
+ } else if (lockEntry.source === "github") {
862
+ latest = await fetchGithubLatest(lockEntry.resolved);
863
+ }
864
+ if (!latest || latest === current) {
865
+ return {
866
+ server: name,
867
+ source: lockEntry.source,
868
+ currentVersion: current,
869
+ latestVersion: latest ?? current,
870
+ hasUpdate: false
871
+ };
872
+ }
873
+ const hasUpdate = compareVersions(current, latest) === -1;
874
+ return {
875
+ server: name,
876
+ source: lockEntry.source,
877
+ currentVersion: current,
878
+ latestVersion: latest,
879
+ hasUpdate,
880
+ updateType: hasUpdate ? detectUpdateType(current, latest) : void 0
881
+ };
882
+ }
883
+ async function checkAllVersions(lockfile) {
884
+ const entries = Object.entries(lockfile.servers);
885
+ if (entries.length === 0) return [];
886
+ const results = [];
887
+ const executing = /* @__PURE__ */ new Set();
888
+ for (const [name, entry] of entries) {
889
+ const p11 = checkVersion(name, entry).then((r) => {
890
+ results.push(r);
891
+ executing.delete(p11);
892
+ });
893
+ executing.add(p11);
894
+ if (executing.size >= 5) {
895
+ await Promise.race(executing);
896
+ }
897
+ }
898
+ await Promise.all(executing);
899
+ return results;
900
+ }
901
+
902
+ // src/core/server-updater.ts
903
+ init_cjs_shims();
904
+
905
+ // src/core/server-resolver.ts
906
+ init_cjs_shims();
907
+
908
+ // src/core/registry.ts
909
+ init_cjs_shims();
910
+ var import_node_crypto = require("crypto");
911
+ function computeIntegrity(resolvedUrl) {
912
+ const hash = (0, import_node_crypto.createHash)("sha512").update(resolvedUrl).digest("base64");
913
+ return `sha512-${hash}`;
914
+ }
915
+ async function resolveFromSmithery(name) {
916
+ const url = `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`;
917
+ let data;
918
+ try {
919
+ const res = await fetch(url, {
920
+ headers: { Accept: "application/json" },
921
+ signal: AbortSignal.timeout(8e3)
922
+ });
923
+ if (res.status === 404) {
924
+ throw new Error(`Server '${name}' not found on Smithery registry`);
925
+ }
926
+ if (!res.ok) {
927
+ throw new Error(`Smithery API error: ${res.status}`);
928
+ }
929
+ data = await res.json();
930
+ } catch (err) {
931
+ if (err instanceof Error && err.message.includes("not found")) throw err;
932
+ throw new Error(
933
+ `Cannot reach Smithery registry: ${err instanceof Error ? err.message : String(err)}`
934
+ );
935
+ }
936
+ const version = typeof data.version === "string" ? data.version : "latest";
937
+ const command = typeof data.command === "string" ? data.command : "npx";
938
+ const args = Array.isArray(data.args) ? data.args : ["-y", `${name}@${version}`];
939
+ const envVars = Array.isArray(data.envVars) ? data.envVars : [];
940
+ const resolved = typeof data.resolved === "string" ? data.resolved : `smithery:${name}@${version}`;
941
+ return {
942
+ name,
943
+ version,
944
+ description: typeof data.description === "string" ? data.description : "",
945
+ runtime: "node",
946
+ command,
947
+ args,
948
+ envVars,
949
+ resolved
950
+ };
951
+ }
952
+ async function resolveFromNpm(packageName) {
953
+ const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
954
+ let data;
955
+ try {
956
+ const res = await fetch(url, {
957
+ headers: { Accept: "application/json" },
958
+ signal: AbortSignal.timeout(8e3)
959
+ });
960
+ if (res.status === 404) {
961
+ throw new Error(`Package '${packageName}' not found on npm`);
962
+ }
963
+ if (!res.ok) {
964
+ throw new Error(`npm registry error: ${res.status}`);
965
+ }
966
+ data = await res.json();
967
+ } catch (err) {
968
+ if (err instanceof Error && err.message.includes("not found")) throw err;
969
+ throw new Error(
970
+ `Cannot reach npm registry: ${err instanceof Error ? err.message : String(err)}`
971
+ );
972
+ }
973
+ const version = typeof data.version === "string" ? data.version : "latest";
974
+ const resolved = `https://registry.npmjs.org/${packageName}/-/${packageName.replace(/^@[^/]+\//, "")}-${version}.tgz`;
975
+ const mcpField = data.mcp && typeof data.mcp === "object" ? data.mcp : null;
976
+ const envVars = mcpField?.envVars ? mcpField.envVars : [];
977
+ return {
978
+ name: packageName,
979
+ version,
980
+ description: typeof data.description === "string" ? data.description : "",
981
+ runtime: "node",
982
+ command: "npx",
983
+ args: ["-y", `${packageName}@${version}`],
984
+ envVars,
985
+ resolved
986
+ };
987
+ }
988
+ async function resolveFromGitHub(githubUrl) {
989
+ const match = githubUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
990
+ if (!match) {
991
+ throw new Error(`Invalid GitHub URL: ${githubUrl}`);
992
+ }
993
+ const [, owner, repo] = match;
994
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/package.json`;
995
+ let pkgData = {};
996
+ try {
997
+ const res = await fetch(rawUrl, { signal: AbortSignal.timeout(8e3) });
998
+ if (res.ok) {
999
+ pkgData = await res.json();
1000
+ }
1001
+ } catch {
1002
+ }
1003
+ const version = typeof pkgData.version === "string" ? pkgData.version : "main";
1004
+ const name = typeof pkgData.name === "string" ? pkgData.name : `${owner}/${repo}`;
1005
+ return {
1006
+ name,
1007
+ version,
1008
+ description: typeof pkgData.description === "string" ? pkgData.description : "",
1009
+ runtime: "node",
1010
+ command: "npx",
1011
+ args: ["-y", githubUrl],
1012
+ envVars: [],
1013
+ resolved: githubUrl
1014
+ };
1015
+ }
1016
+
1017
+ // src/core/server-resolver.ts
1018
+ function detectSource(input) {
1019
+ if (input.startsWith("smithery:")) {
1020
+ return { type: "smithery", input: input.slice(9) };
1021
+ }
1022
+ if (input.startsWith("https://github.com/") || input.startsWith("github.com/")) {
1023
+ return { type: "github", input };
1024
+ }
1025
+ return { type: "npm", input };
1026
+ }
1027
+ function parseEnvFlags(envFlags) {
1028
+ if (!envFlags) return {};
1029
+ const flags = Array.isArray(envFlags) ? envFlags : [envFlags];
1030
+ const result = {};
1031
+ for (const flag of flags) {
1032
+ const idx = flag.indexOf("=");
1033
+ if (idx > 0) {
1034
+ result[flag.slice(0, idx)] = flag.slice(idx + 1);
1035
+ }
1036
+ }
1037
+ return result;
1038
+ }
1039
+ async function resolveServer(input) {
1040
+ const source = detectSource(input);
1041
+ switch (source.type) {
1042
+ case "smithery":
1043
+ return resolveFromSmithery(source.input);
1044
+ case "github":
1045
+ return resolveFromGitHub(source.input);
1046
+ case "npm":
1047
+ return resolveFromNpm(source.input);
1048
+ }
1049
+ }
1050
+
1051
+ // src/core/server-updater.ts
1052
+ async function applyServerUpdate(serverName, lockEntry, clients) {
1053
+ const fromVersion = lockEntry.version;
1054
+ const input = lockEntry.source === "smithery" ? `smithery:${serverName}` : lockEntry.source === "github" ? lockEntry.resolved : serverName;
1055
+ try {
1056
+ const metadata = await resolveServer(input);
1057
+ const integrity = computeIntegrity(metadata.resolved);
1058
+ addEntry(serverName, {
1059
+ ...lockEntry,
1060
+ version: metadata.version,
1061
+ resolved: metadata.resolved,
1062
+ integrity,
1063
+ command: metadata.command,
1064
+ args: metadata.args,
1065
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
1066
+ });
1067
+ const targetClients = clients.filter(
1068
+ (c) => lockEntry.clients.includes(c.type)
1069
+ );
1070
+ for (const client of targetClients) {
1071
+ try {
1072
+ await client.addServer(serverName, {
1073
+ command: metadata.command,
1074
+ args: metadata.args
1075
+ });
1076
+ } catch {
1077
+ }
1078
+ }
1079
+ return {
1080
+ server: serverName,
1081
+ success: true,
1082
+ fromVersion,
1083
+ toVersion: metadata.version
1084
+ };
1085
+ } catch (err) {
1086
+ return {
1087
+ server: serverName,
1088
+ success: false,
1089
+ fromVersion,
1090
+ toVersion: fromVersion,
1091
+ error: err instanceof Error ? err.message : String(err)
1092
+ };
1093
+ }
1094
+ }
1095
+
780
1096
  // src/commands/audit.ts
781
1097
  function colorRisk(level, score) {
782
1098
  const label = score !== null ? `${score}/100 (${level})` : level;
@@ -847,7 +1163,12 @@ var audit_default = (0, import_citty.defineCommand)({
847
1163
  },
848
1164
  fix: {
849
1165
  type: "boolean",
850
- description: "Show available fix versions for vulnerable packages",
1166
+ description: "Apply updates to fix vulnerable packages",
1167
+ default: false
1168
+ },
1169
+ yes: {
1170
+ type: "boolean",
1171
+ description: "Skip confirmation prompt (use with --fix)",
851
1172
  default: false
852
1173
  }
853
1174
  },
@@ -908,17 +1229,110 @@ var audit_default = (0, import_citty.defineCommand)({
908
1229
  Summary: ${parts.join(" | ")}
909
1230
  `);
910
1231
  if (args.fix) {
911
- const withVulns = reports.filter((r) => r.vulnerabilities.length > 0);
912
- if (withVulns.length > 0) {
913
- console.log(import_picocolors.default.bold(" Fix suggestions:"));
914
- for (const r of withVulns) {
915
- console.log(` ${import_picocolors.default.cyan("\u2192")} Run ${import_picocolors.default.cyan(`mcpman install ${r.server}@latest`)} to update`);
916
- }
917
- console.log();
918
- }
1232
+ await runAuditFix(reports, lockfile.servers, args.yes);
919
1233
  }
920
1234
  }
921
1235
  });
1236
+ async function loadClients() {
1237
+ try {
1238
+ const mod = await Promise.resolve().then(() => (init_client_detector(), client_detector_exports));
1239
+ return mod.getInstalledClients();
1240
+ } catch {
1241
+ return [];
1242
+ }
1243
+ }
1244
+ async function runAuditFix(reports, servers, skipConfirm) {
1245
+ const npmWithVulns = reports.filter(
1246
+ (r) => r.vulnerabilities.length > 0 && r.source === "npm"
1247
+ );
1248
+ const nonNpmWithVulns = reports.filter(
1249
+ (r) => r.vulnerabilities.length > 0 && r.source !== "npm"
1250
+ );
1251
+ if (nonNpmWithVulns.length > 0) {
1252
+ console.log(import_picocolors.default.yellow(" Non-npm servers require manual update:"));
1253
+ for (const r of nonNpmWithVulns) {
1254
+ console.log(` ${import_picocolors.default.dim("\u2192")} ${r.server} (${r.source})`);
1255
+ }
1256
+ console.log();
1257
+ }
1258
+ if (npmWithVulns.length === 0) {
1259
+ console.log(import_picocolors.default.green(" No fixable vulnerabilities found.\n"));
1260
+ return;
1261
+ }
1262
+ const versionSpinner = (0, import_nanospinner.createSpinner)("Checking for available updates...").start();
1263
+ const versionChecks = await Promise.all(
1264
+ npmWithVulns.map((r) => checkVersion(r.server, servers[r.server]))
1265
+ );
1266
+ versionSpinner.success({ text: "Version check complete" });
1267
+ const updatable = versionChecks.filter((u) => u.hasUpdate);
1268
+ if (updatable.length === 0) {
1269
+ console.log(import_picocolors.default.yellow(
1270
+ " Vulnerable servers have no newer versions available yet.\n Allow time for registry to publish fixes.\n"
1271
+ ));
1272
+ return;
1273
+ }
1274
+ console.log(import_picocolors.default.bold(`
1275
+ ${updatable.length} server(s) can be updated to fix vulnerabilities:
1276
+ `));
1277
+ for (const u of updatable) {
1278
+ console.log(` ${import_picocolors.default.cyan("\u2192")} ${u.server} ${import_picocolors.default.dim(u.currentVersion)} \u2192 ${import_picocolors.default.green(u.latestVersion)}`);
1279
+ }
1280
+ console.log();
1281
+ if (!skipConfirm) {
1282
+ const confirmed = await p.confirm({
1283
+ message: `Update ${updatable.length} vulnerable server(s)?`,
1284
+ initialValue: true
1285
+ });
1286
+ if (p.isCancel(confirmed) || !confirmed) {
1287
+ p.outro("Cancelled.");
1288
+ return;
1289
+ }
1290
+ }
1291
+ const clients = await loadClients();
1292
+ let successCount = 0;
1293
+ const results = [];
1294
+ for (const u of updatable) {
1295
+ const s = (0, import_nanospinner.createSpinner)(`Updating ${u.server}...`).start();
1296
+ const result = await applyServerUpdate(u.server, servers[u.server], clients);
1297
+ if (result.success) {
1298
+ s.success({ text: `${import_picocolors.default.green("\u2713")} ${u.server}: ${result.fromVersion} \u2192 ${result.toVersion}` });
1299
+ successCount++;
1300
+ } else {
1301
+ s.error({ text: `${import_picocolors.default.red("\u2717")} ${u.server}: ${result.error}` });
1302
+ }
1303
+ results.push({
1304
+ server: u.server,
1305
+ from: result.fromVersion,
1306
+ to: result.toVersion,
1307
+ ok: result.success,
1308
+ error: result.error
1309
+ });
1310
+ }
1311
+ console.log();
1312
+ if (successCount > 0) {
1313
+ const updatedNames = results.filter((r) => r.ok).map((r) => r.server);
1314
+ const freshLockfile = readLockfile();
1315
+ const rescanSpinner = (0, import_nanospinner.createSpinner)("Re-scanning updated servers...").start();
1316
+ const afterReports = await Promise.all(
1317
+ updatedNames.map((name) => scanServer(name, freshLockfile.servers[name]))
1318
+ );
1319
+ rescanSpinner.success({ text: "Re-scan complete" });
1320
+ console.log(import_picocolors.default.bold("\n Before / After:\n"));
1321
+ for (const after of afterReports) {
1322
+ const before = reports.find((r) => r.server === after.server);
1323
+ const beforeVulns = before?.vulnerabilities.length ?? 0;
1324
+ const afterVulns = after.vulnerabilities.length;
1325
+ const improved = afterVulns < beforeVulns ? import_picocolors.default.green("improved") : import_picocolors.default.yellow("unchanged");
1326
+ console.log(
1327
+ ` ${import_picocolors.default.bold(after.server)} vulns: ${beforeVulns} \u2192 ${afterVulns} [${improved}]`
1328
+ );
1329
+ }
1330
+ console.log();
1331
+ }
1332
+ console.log(`
1333
+ ${successCount} of ${updatable.length} server(s) updated.
1334
+ `);
1335
+ }
922
1336
 
923
1337
  // src/commands/doctor.ts
924
1338
  init_cjs_shims();
@@ -1252,141 +1666,30 @@ function printServerResult(result, showFix) {
1252
1666
  console.log(` ${import_picocolors2.default.yellow("\u2192")} Fix: ${import_picocolors2.default.cyan(check.fix)}`);
1253
1667
  }
1254
1668
  }
1255
- console.log();
1256
- }
1257
- async function runParallel(tasks, concurrency) {
1258
- const results = [];
1259
- const executing = /* @__PURE__ */ new Set();
1260
- for (const task of tasks) {
1261
- const p9 = task().then((r) => {
1262
- results.push(r);
1263
- executing.delete(p9);
1264
- });
1265
- executing.add(p9);
1266
- if (executing.size >= concurrency) {
1267
- await Promise.race(executing);
1268
- }
1269
- }
1270
- await Promise.all(executing);
1271
- return results;
1272
- }
1273
-
1274
- // src/commands/init.ts
1275
- init_cjs_shims();
1276
- var import_citty3 = require("citty");
1277
- var p = __toESM(require("@clack/prompts"), 1);
1278
- var import_node_path5 = __toESM(require("path"), 1);
1279
-
1280
- // src/core/registry.ts
1281
- init_cjs_shims();
1282
- var import_node_crypto = require("crypto");
1283
- function computeIntegrity(resolvedUrl) {
1284
- const hash = (0, import_node_crypto.createHash)("sha512").update(resolvedUrl).digest("base64");
1285
- return `sha512-${hash}`;
1286
- }
1287
- async function resolveFromSmithery(name) {
1288
- const url = `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`;
1289
- let data;
1290
- try {
1291
- const res = await fetch(url, {
1292
- headers: { Accept: "application/json" },
1293
- signal: AbortSignal.timeout(8e3)
1294
- });
1295
- if (res.status === 404) {
1296
- throw new Error(`Server '${name}' not found on Smithery registry`);
1297
- }
1298
- if (!res.ok) {
1299
- throw new Error(`Smithery API error: ${res.status}`);
1300
- }
1301
- data = await res.json();
1302
- } catch (err) {
1303
- if (err instanceof Error && err.message.includes("not found")) throw err;
1304
- throw new Error(
1305
- `Cannot reach Smithery registry: ${err instanceof Error ? err.message : String(err)}`
1306
- );
1307
- }
1308
- const version = typeof data.version === "string" ? data.version : "latest";
1309
- const command = typeof data.command === "string" ? data.command : "npx";
1310
- const args = Array.isArray(data.args) ? data.args : ["-y", `${name}@${version}`];
1311
- const envVars = Array.isArray(data.envVars) ? data.envVars : [];
1312
- const resolved = typeof data.resolved === "string" ? data.resolved : `smithery:${name}@${version}`;
1313
- return {
1314
- name,
1315
- version,
1316
- description: typeof data.description === "string" ? data.description : "",
1317
- runtime: "node",
1318
- command,
1319
- args,
1320
- envVars,
1321
- resolved
1322
- };
1323
- }
1324
- async function resolveFromNpm(packageName) {
1325
- const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
1326
- let data;
1327
- try {
1328
- const res = await fetch(url, {
1329
- headers: { Accept: "application/json" },
1330
- signal: AbortSignal.timeout(8e3)
1331
- });
1332
- if (res.status === 404) {
1333
- throw new Error(`Package '${packageName}' not found on npm`);
1334
- }
1335
- if (!res.ok) {
1336
- throw new Error(`npm registry error: ${res.status}`);
1337
- }
1338
- data = await res.json();
1339
- } catch (err) {
1340
- if (err instanceof Error && err.message.includes("not found")) throw err;
1341
- throw new Error(
1342
- `Cannot reach npm registry: ${err instanceof Error ? err.message : String(err)}`
1343
- );
1344
- }
1345
- const version = typeof data.version === "string" ? data.version : "latest";
1346
- const resolved = `https://registry.npmjs.org/${packageName}/-/${packageName.replace(/^@[^/]+\//, "")}-${version}.tgz`;
1347
- const mcpField = data.mcp && typeof data.mcp === "object" ? data.mcp : null;
1348
- const envVars = mcpField?.envVars ? mcpField.envVars : [];
1349
- return {
1350
- name: packageName,
1351
- version,
1352
- description: typeof data.description === "string" ? data.description : "",
1353
- runtime: "node",
1354
- command: "npx",
1355
- args: ["-y", `${packageName}@${version}`],
1356
- envVars,
1357
- resolved
1358
- };
1669
+ console.log();
1359
1670
  }
1360
- async function resolveFromGitHub(githubUrl) {
1361
- const match = githubUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
1362
- if (!match) {
1363
- throw new Error(`Invalid GitHub URL: ${githubUrl}`);
1364
- }
1365
- const [, owner, repo] = match;
1366
- const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/package.json`;
1367
- let pkgData = {};
1368
- try {
1369
- const res = await fetch(rawUrl, { signal: AbortSignal.timeout(8e3) });
1370
- if (res.ok) {
1371
- pkgData = await res.json();
1671
+ async function runParallel(tasks, concurrency) {
1672
+ const results = [];
1673
+ const executing = /* @__PURE__ */ new Set();
1674
+ for (const task of tasks) {
1675
+ const p11 = task().then((r) => {
1676
+ results.push(r);
1677
+ executing.delete(p11);
1678
+ });
1679
+ executing.add(p11);
1680
+ if (executing.size >= concurrency) {
1681
+ await Promise.race(executing);
1372
1682
  }
1373
- } catch {
1374
1683
  }
1375
- const version = typeof pkgData.version === "string" ? pkgData.version : "main";
1376
- const name = typeof pkgData.name === "string" ? pkgData.name : `${owner}/${repo}`;
1377
- return {
1378
- name,
1379
- version,
1380
- description: typeof pkgData.description === "string" ? pkgData.description : "",
1381
- runtime: "node",
1382
- command: "npx",
1383
- args: ["-y", githubUrl],
1384
- envVars: [],
1385
- resolved: githubUrl
1386
- };
1684
+ await Promise.all(executing);
1685
+ return results;
1387
1686
  }
1388
1687
 
1389
1688
  // src/commands/init.ts
1689
+ init_cjs_shims();
1690
+ var import_citty3 = require("citty");
1691
+ var p2 = __toESM(require("@clack/prompts"), 1);
1692
+ var import_node_path5 = __toESM(require("path"), 1);
1390
1693
  var init_default = (0, import_citty3.defineCommand)({
1391
1694
  meta: {
1392
1695
  name: "init",
@@ -1402,17 +1705,17 @@ var init_default = (0, import_citty3.defineCommand)({
1402
1705
  },
1403
1706
  async run({ args }) {
1404
1707
  const nonInteractive = args.yes || !process.stdout.isTTY;
1405
- p.intro("mcpman init");
1708
+ p2.intro("mcpman init");
1406
1709
  const targetPath = import_node_path5.default.join(process.cwd(), LOCKFILE_NAME);
1407
1710
  const existing = findLockfile();
1408
1711
  if (existing) {
1409
1712
  if (nonInteractive) {
1410
- p.log.warn(`Lockfile already exists: ${existing} \u2014 overwriting (non-interactive).`);
1713
+ p2.log.warn(`Lockfile already exists: ${existing} \u2014 overwriting (non-interactive).`);
1411
1714
  } else {
1412
- p.log.warn(`Lockfile already exists: ${existing}`);
1413
- const overwrite = await p.confirm({ message: "Overwrite?" });
1414
- if (p.isCancel(overwrite) || !overwrite) {
1415
- p.outro("Cancelled.");
1715
+ p2.log.warn(`Lockfile already exists: ${existing}`);
1716
+ const overwrite = await p2.confirm({ message: "Overwrite?" });
1717
+ if (p2.isCancel(overwrite) || !overwrite) {
1718
+ p2.outro("Cancelled.");
1416
1719
  return;
1417
1720
  }
1418
1721
  }
@@ -1422,7 +1725,7 @@ var init_default = (0, import_citty3.defineCommand)({
1422
1725
  const mod = await Promise.resolve().then(() => (init_client_detector(), client_detector_exports));
1423
1726
  clients = await mod.getInstalledClients();
1424
1727
  } catch {
1425
- p.log.warn("Could not detect AI clients \u2014 creating empty lockfile.");
1728
+ p2.log.warn("Could not detect AI clients \u2014 creating empty lockfile.");
1426
1729
  }
1427
1730
  const clientServers = [];
1428
1731
  for (const client of clients) {
@@ -1436,26 +1739,26 @@ var init_default = (0, import_citty3.defineCommand)({
1436
1739
  }
1437
1740
  createEmptyLockfile(targetPath);
1438
1741
  if (clientServers.length === 0) {
1439
- p.log.info("No existing servers found in any client config.");
1440
- p.outro(`Created ${LOCKFILE_NAME} \u2014 add it to version control!`);
1742
+ p2.log.info("No existing servers found in any client config.");
1743
+ p2.outro(`Created ${LOCKFILE_NAME} \u2014 add it to version control!`);
1441
1744
  return;
1442
1745
  }
1443
1746
  let selected;
1444
1747
  if (nonInteractive) {
1445
1748
  selected = clientServers.map((cs) => cs.client.type);
1446
- p.log.info(`Non-interactive mode: importing all ${clientServers.length} client(s).`);
1749
+ p2.log.info(`Non-interactive mode: importing all ${clientServers.length} client(s).`);
1447
1750
  } else {
1448
1751
  const options = clientServers.map((cs) => ({
1449
1752
  value: cs.client.type,
1450
1753
  label: `${cs.client.displayName} (${Object.keys(cs.servers).length} servers)`
1451
1754
  }));
1452
- const toImport = await p.multiselect({
1755
+ const toImport = await p2.multiselect({
1453
1756
  message: "Import existing servers into lockfile?",
1454
1757
  options,
1455
1758
  required: false
1456
1759
  });
1457
- if (p.isCancel(toImport)) {
1458
- p.outro(`Created empty ${LOCKFILE_NAME}`);
1760
+ if (p2.isCancel(toImport)) {
1761
+ p2.outro(`Created empty ${LOCKFILE_NAME}`);
1459
1762
  return;
1460
1763
  }
1461
1764
  selected = toImport;
@@ -1481,7 +1784,7 @@ var init_default = (0, import_citty3.defineCommand)({
1481
1784
  importCount++;
1482
1785
  }
1483
1786
  }
1484
- p.outro(
1787
+ p2.outro(
1485
1788
  `Created ${LOCKFILE_NAME} with ${importCount} server(s) \u2014 commit to version control!`
1486
1789
  );
1487
1790
  }
@@ -1493,45 +1796,44 @@ var import_citty4 = require("citty");
1493
1796
 
1494
1797
  // src/core/installer.ts
1495
1798
  init_cjs_shims();
1496
- var p2 = __toESM(require("@clack/prompts"), 1);
1799
+ var p5 = __toESM(require("@clack/prompts"), 1);
1497
1800
 
1498
- // src/core/server-resolver.ts
1801
+ // src/core/installer-vault-helpers.ts
1499
1802
  init_cjs_shims();
1500
- function detectSource(input) {
1501
- if (input.startsWith("smithery:")) {
1502
- return { type: "smithery", input: input.slice(9) };
1503
- }
1504
- if (input.startsWith("https://github.com/") || input.startsWith("github.com/")) {
1505
- return { type: "github", input };
1506
- }
1507
- return { type: "npm", input };
1508
- }
1509
- function parseEnvFlags(envFlags) {
1510
- if (!envFlags) return {};
1511
- const flags = Array.isArray(envFlags) ? envFlags : [envFlags];
1512
- const result = {};
1513
- for (const flag of flags) {
1514
- const idx = flag.indexOf("=");
1515
- if (idx > 0) {
1516
- result[flag.slice(0, idx)] = flag.slice(idx + 1);
1803
+ var p4 = __toESM(require("@clack/prompts"), 1);
1804
+ init_vault_service();
1805
+ async function tryLoadVaultSecrets(serverName) {
1806
+ try {
1807
+ const entries = listSecrets(serverName);
1808
+ if (entries.length === 0 || entries[0].keys.length === 0) {
1809
+ return {};
1517
1810
  }
1811
+ const password2 = await getMasterPassword();
1812
+ return getSecretsForServer(serverName, password2);
1813
+ } catch {
1814
+ return {};
1518
1815
  }
1519
- return result;
1520
1816
  }
1521
- async function resolveServer(input) {
1522
- const source = detectSource(input);
1523
- switch (source.type) {
1524
- case "smithery":
1525
- return resolveFromSmithery(source.input);
1526
- case "github":
1527
- return resolveFromGitHub(source.input);
1528
- case "npm":
1529
- return resolveFromNpm(source.input);
1817
+ async function offerVaultSave(serverName, newVars, yes) {
1818
+ if (Object.keys(newVars).length === 0) return;
1819
+ if (yes) return;
1820
+ try {
1821
+ const save = await p4.confirm({
1822
+ message: `Save ${Object.keys(newVars).length} env var(s) to encrypted vault for future installs?`
1823
+ });
1824
+ if (p4.isCancel(save) || !save) return;
1825
+ const password2 = await getMasterPassword();
1826
+ for (const [key, value] of Object.entries(newVars)) {
1827
+ setSecret(serverName, key, value, password2);
1828
+ }
1829
+ p4.log.success(`Credentials saved to vault for '${serverName}'`);
1830
+ } catch (err) {
1831
+ p4.log.warn(`Could not save to vault: ${err instanceof Error ? err.message : String(err)}`);
1530
1832
  }
1531
1833
  }
1532
1834
 
1533
1835
  // src/core/installer.ts
1534
- async function loadClients() {
1836
+ async function loadClients2() {
1535
1837
  try {
1536
1838
  const mod = await Promise.resolve().then(() => (init_client_detector(), client_detector_exports));
1537
1839
  return mod.getInstalledClients();
@@ -1540,65 +1842,68 @@ async function loadClients() {
1540
1842
  }
1541
1843
  }
1542
1844
  async function installServer(input, options = {}) {
1543
- p2.intro("mcpman install");
1544
- const spinner5 = p2.spinner();
1845
+ p5.intro("mcpman install");
1846
+ const spinner5 = p5.spinner();
1545
1847
  spinner5.start("Resolving server...");
1546
1848
  let metadata;
1547
1849
  try {
1548
1850
  metadata = await resolveServer(input);
1549
1851
  } catch (err) {
1550
1852
  spinner5.stop("Resolution failed");
1551
- p2.log.error(err instanceof Error ? err.message : String(err));
1853
+ p5.log.error(err instanceof Error ? err.message : String(err));
1552
1854
  process.exit(1);
1553
1855
  }
1554
1856
  spinner5.stop(`Found: ${metadata.name}@${metadata.version}`);
1555
- const clients = await loadClients();
1857
+ const clients = await loadClients2();
1556
1858
  if (clients.length === 0) {
1557
- p2.log.warn("No supported AI clients detected on this machine.");
1558
- p2.log.info("Supported: Claude Desktop, Cursor, VS Code, Windsurf");
1859
+ p5.log.warn("No supported AI clients detected on this machine.");
1860
+ p5.log.info("Supported: Claude Desktop, Cursor, VS Code, Windsurf");
1559
1861
  process.exit(1);
1560
1862
  }
1561
1863
  let selectedClients;
1562
1864
  if (options.client) {
1563
1865
  const found = clients.find((c) => c.type === options.client || c.displayName.toLowerCase() === options.client?.toLowerCase());
1564
1866
  if (!found) {
1565
- p2.log.error(`Client '${options.client}' not found or not installed.`);
1566
- p2.log.info(`Available: ${clients.map((c) => c.type).join(", ")}`);
1867
+ p5.log.error(`Client '${options.client}' not found or not installed.`);
1868
+ p5.log.info(`Available: ${clients.map((c) => c.type).join(", ")}`);
1567
1869
  process.exit(1);
1568
1870
  }
1569
1871
  selectedClients = [found];
1570
1872
  } else if (options.yes || clients.length === 1) {
1571
1873
  selectedClients = clients;
1572
1874
  } else {
1573
- const chosen = await p2.multiselect({
1875
+ const chosen = await p5.multiselect({
1574
1876
  message: "Install to which client(s)?",
1575
1877
  options: clients.map((c) => ({ value: c.type, label: c.displayName })),
1576
1878
  required: true
1577
1879
  });
1578
- if (p2.isCancel(chosen)) {
1579
- p2.outro("Cancelled.");
1880
+ if (p5.isCancel(chosen)) {
1881
+ p5.outro("Cancelled.");
1580
1882
  process.exit(0);
1581
1883
  }
1582
1884
  selectedClients = clients.filter((c) => chosen.includes(c.type));
1583
1885
  }
1584
1886
  const providedEnv = parseEnvFlags(options.env);
1585
- const collectedEnv = { ...providedEnv };
1887
+ const vaultEnv = await tryLoadVaultSecrets(metadata.name);
1888
+ const collectedEnv = { ...vaultEnv, ...providedEnv };
1889
+ const newlyEnteredVars = {};
1586
1890
  const requiredVars = metadata.envVars.filter((e) => e.required && !(e.name in collectedEnv));
1587
1891
  for (const envVar of requiredVars) {
1588
1892
  if (options.yes && envVar.default) {
1589
1893
  collectedEnv[envVar.name] = envVar.default;
1590
1894
  continue;
1591
1895
  }
1592
- const val = await p2.text({
1896
+ const val = await p5.text({
1593
1897
  message: `${envVar.name}${envVar.description ? ` \u2014 ${envVar.description}` : ""}`,
1594
1898
  placeholder: envVar.default ?? "",
1595
1899
  validate: (v) => envVar.required && !v ? "Required" : void 0
1596
1900
  });
1597
- if (p2.isCancel(val)) {
1598
- p2.outro("Cancelled.");
1901
+ if (p5.isCancel(val)) {
1902
+ p5.outro("Cancelled.");
1599
1903
  process.exit(0);
1600
1904
  }
1601
1905
  collectedEnv[envVar.name] = val;
1906
+ newlyEnteredVars[envVar.name] = val;
1602
1907
  }
1603
1908
  const entry = {
1604
1909
  command: metadata.command,
@@ -1613,7 +1918,7 @@ async function installServer(input, options = {}) {
1613
1918
  clientTypes.push(client.type);
1614
1919
  } catch (err) {
1615
1920
  spinner5.stop("Partial failure");
1616
- p2.log.warn(`Failed to write to ${client.displayName}: ${err instanceof Error ? err.message : String(err)}`);
1921
+ p5.log.warn(`Failed to write to ${client.displayName}: ${err instanceof Error ? err.message : String(err)}`);
1617
1922
  }
1618
1923
  }
1619
1924
  spinner5.stop("Config written");
@@ -1632,8 +1937,9 @@ async function installServer(input, options = {}) {
1632
1937
  clients: clientTypes
1633
1938
  });
1634
1939
  const lockPath = findLockfile() ?? "mcpman.lock (global)";
1635
- p2.log.success(`Lockfile updated: ${lockPath}`);
1636
- p2.outro(`${metadata.name}@${metadata.version} installed to ${clientTypes.join(", ")}`);
1940
+ p5.log.success(`Lockfile updated: ${lockPath}`);
1941
+ await offerVaultSave(metadata.name, newlyEnteredVars, options.yes ?? false);
1942
+ p5.outro(`${metadata.name}@${metadata.version} installed to ${clientTypes.join(", ")}`);
1637
1943
  }
1638
1944
 
1639
1945
  // src/utils/logger.ts
@@ -1658,7 +1964,7 @@ function json(data) {
1658
1964
  }
1659
1965
 
1660
1966
  // src/commands/install.ts
1661
- var p3 = __toESM(require("@clack/prompts"), 1);
1967
+ var p6 = __toESM(require("@clack/prompts"), 1);
1662
1968
  var install_default = (0, import_citty4.defineCommand)({
1663
1969
  meta: {
1664
1970
  name: "install",
@@ -1708,8 +2014,8 @@ async function restoreFromLockfile() {
1708
2014
  info("Lockfile is empty \u2014 nothing to restore.");
1709
2015
  return;
1710
2016
  }
1711
- p3.intro(`mcpman install (restore from ${lockPath})`);
1712
- p3.log.info(`Restoring ${entries.length} server(s)...`);
2017
+ p6.intro(`mcpman install (restore from ${lockPath})`);
2018
+ p6.log.info(`Restoring ${entries.length} server(s)...`);
1713
2019
  for (const [name, entry] of entries) {
1714
2020
  const input = entry.source === "smithery" ? `smithery:${name}` : entry.source === "github" ? entry.resolved : name;
1715
2021
  await installServer(input, {
@@ -1717,7 +2023,7 @@ async function restoreFromLockfile() {
1717
2023
  yes: true
1718
2024
  });
1719
2025
  }
1720
- p3.outro("Restore complete.");
2026
+ p6.outro("Restore complete.");
1721
2027
  }
1722
2028
 
1723
2029
  // src/commands/list.ts
@@ -1805,7 +2111,7 @@ function formatClients(clients) {
1805
2111
  // src/commands/remove.ts
1806
2112
  init_cjs_shims();
1807
2113
  var import_citty6 = require("citty");
1808
- var p4 = __toESM(require("@clack/prompts"), 1);
2114
+ var p7 = __toESM(require("@clack/prompts"), 1);
1809
2115
  var import_picocolors5 = __toESM(require("picocolors"), 1);
1810
2116
  init_client_detector();
1811
2117
  var CLIENT_DISPLAY2 = {
@@ -1844,17 +2150,17 @@ var remove_default = (0, import_citty6.defineCommand)({
1844
2150
  }
1845
2151
  },
1846
2152
  async run({ args }) {
1847
- p4.intro(import_picocolors5.default.bold("mcpman remove"));
2153
+ p7.intro(import_picocolors5.default.bold("mcpman remove"));
1848
2154
  const serverName = args.server;
1849
2155
  const servers = await getInstalledServers();
1850
2156
  const match = servers.find((s) => s.name === serverName);
1851
2157
  if (!match) {
1852
- p4.log.warn(`Server "${serverName}" is not installed.`);
2158
+ p7.log.warn(`Server "${serverName}" is not installed.`);
1853
2159
  const similar = servers.filter((s) => s.name.includes(serverName) || serverName.includes(s.name));
1854
2160
  if (similar.length > 0) {
1855
- p4.log.info(`Did you mean: ${similar.map((s) => import_picocolors5.default.cyan(s.name)).join(", ")}?`);
2161
+ p7.log.info(`Did you mean: ${similar.map((s) => import_picocolors5.default.cyan(s.name)).join(", ")}?`);
1856
2162
  }
1857
- p4.outro("Nothing to remove.");
2163
+ p7.outro("Nothing to remove.");
1858
2164
  return;
1859
2165
  }
1860
2166
  let targetClients;
@@ -1862,15 +2168,15 @@ var remove_default = (0, import_citty6.defineCommand)({
1862
2168
  targetClients = match.clients;
1863
2169
  } else if (args.client) {
1864
2170
  if (!match.clients.includes(args.client)) {
1865
- p4.log.warn(`Server "${serverName}" is not installed in client "${args.client}".`);
1866
- p4.outro("Nothing to remove.");
2171
+ p7.log.warn(`Server "${serverName}" is not installed in client "${args.client}".`);
2172
+ p7.outro("Nothing to remove.");
1867
2173
  return;
1868
2174
  }
1869
2175
  targetClients = [args.client];
1870
2176
  } else if (match.clients.length === 1) {
1871
2177
  targetClients = match.clients;
1872
2178
  } else {
1873
- const selected = await p4.multiselect({
2179
+ const selected = await p7.multiselect({
1874
2180
  message: `Remove "${serverName}" from which clients?`,
1875
2181
  options: match.clients.map((c) => ({
1876
2182
  value: c,
@@ -1878,19 +2184,19 @@ var remove_default = (0, import_citty6.defineCommand)({
1878
2184
  })),
1879
2185
  required: true
1880
2186
  });
1881
- if (p4.isCancel(selected)) {
1882
- p4.outro("Cancelled.");
2187
+ if (p7.isCancel(selected)) {
2188
+ p7.outro("Cancelled.");
1883
2189
  process.exit(0);
1884
2190
  }
1885
2191
  targetClients = selected;
1886
2192
  }
1887
2193
  if (!args.yes) {
1888
2194
  const clientNames = targetClients.map(clientDisplayName).join(", ");
1889
- const confirmed = await p4.confirm({
2195
+ const confirmed = await p7.confirm({
1890
2196
  message: `Remove ${import_picocolors5.default.cyan(serverName)} from ${import_picocolors5.default.yellow(clientNames)}?`
1891
2197
  });
1892
- if (p4.isCancel(confirmed) || !confirmed) {
1893
- p4.outro("Cancelled.");
2198
+ if (p7.isCancel(confirmed) || !confirmed) {
2199
+ p7.outro("Cancelled.");
1894
2200
  return;
1895
2201
  }
1896
2202
  }
@@ -1904,18 +2210,18 @@ var remove_default = (0, import_citty6.defineCommand)({
1904
2210
  }
1905
2211
  try {
1906
2212
  await handler.removeServer(serverName);
1907
- p4.log.success(`Removed from ${clientDisplayName(clientType)}`);
2213
+ p7.log.success(`Removed from ${clientDisplayName(clientType)}`);
1908
2214
  } catch (err) {
1909
2215
  const msg = err instanceof Error ? err.message : String(err);
1910
2216
  errors.push(`${clientDisplayName(clientType)}: ${msg}`);
1911
2217
  }
1912
2218
  }
1913
2219
  if (errors.length > 0) {
1914
- for (const e of errors) p4.log.error(e);
1915
- p4.outro(import_picocolors5.default.red("Completed with errors."));
2220
+ for (const e of errors) p7.log.error(e);
2221
+ p7.outro(import_picocolors5.default.red("Completed with errors."));
1916
2222
  process.exit(1);
1917
2223
  }
1918
- p4.outro(import_picocolors5.default.green(`Removed "${serverName}" successfully.`));
2224
+ p7.outro(import_picocolors5.default.green(`Removed "${serverName}" successfully.`));
1919
2225
  }
1920
2226
  });
1921
2227
 
@@ -1923,7 +2229,7 @@ var remove_default = (0, import_citty6.defineCommand)({
1923
2229
  init_cjs_shims();
1924
2230
  var import_citty7 = require("citty");
1925
2231
  var import_picocolors6 = __toESM(require("picocolors"), 1);
1926
- var p6 = __toESM(require("@clack/prompts"), 1);
2232
+ var p8 = __toESM(require("@clack/prompts"), 1);
1927
2233
  init_vault_service();
1928
2234
  function maskValue(value) {
1929
2235
  if (value.length <= 8) return "***";
@@ -1954,12 +2260,12 @@ var setCommand = (0, import_citty7.defineCommand)({
1954
2260
  console.error(import_picocolors6.default.red("\u2717") + " Invalid format. Expected KEY=VALUE");
1955
2261
  process.exit(1);
1956
2262
  }
1957
- p6.intro(import_picocolors6.default.cyan("mcpman secrets set"));
2263
+ p8.intro(import_picocolors6.default.cyan("mcpman secrets set"));
1958
2264
  const isNew = listSecrets(args.server).length === 0 || !listSecrets(args.server)[0]?.keys.includes(parsed.key);
1959
2265
  const vaultPath = (await Promise.resolve().then(() => (init_vault_service(), vault_service_exports))).getVaultPath();
1960
2266
  const vaultExists = (await import("fs")).existsSync(vaultPath);
1961
2267
  const password2 = await getMasterPassword(!vaultExists && isNew);
1962
- const spin = p6.spinner();
2268
+ const spin = p8.spinner();
1963
2269
  spin.start("Encrypting secret...");
1964
2270
  try {
1965
2271
  setSecret(args.server, parsed.key, parsed.value, password2);
@@ -1971,7 +2277,7 @@ var setCommand = (0, import_citty7.defineCommand)({
1971
2277
  console.error(import_picocolors6.default.dim(String(err)));
1972
2278
  process.exit(1);
1973
2279
  }
1974
- p6.outro(import_picocolors6.default.dim("Secret encrypted and saved to vault."));
2280
+ p8.outro(import_picocolors6.default.dim("Secret encrypted and saved to vault."));
1975
2281
  }
1976
2282
  });
1977
2283
  var listCommand = (0, import_citty7.defineCommand)({
@@ -2017,12 +2323,12 @@ var removeCommand = (0, import_citty7.defineCommand)({
2017
2323
  }
2018
2324
  },
2019
2325
  async run({ args }) {
2020
- const confirmed = await p6.confirm({
2326
+ const confirmed = await p8.confirm({
2021
2327
  message: `Remove ${import_picocolors6.default.bold(args.key)} from ${import_picocolors6.default.cyan(args.server)}?`,
2022
2328
  initialValue: false
2023
2329
  });
2024
- if (p6.isCancel(confirmed) || !confirmed) {
2025
- p6.cancel("Cancelled.");
2330
+ if (p8.isCancel(confirmed) || !confirmed) {
2331
+ p8.cancel("Cancelled.");
2026
2332
  return;
2027
2333
  }
2028
2334
  try {
@@ -2050,7 +2356,7 @@ var secrets_default = (0, import_citty7.defineCommand)({
2050
2356
  // src/commands/sync.ts
2051
2357
  init_cjs_shims();
2052
2358
  var import_citty8 = require("citty");
2053
- var p7 = __toESM(require("@clack/prompts"), 1);
2359
+ var p9 = __toESM(require("@clack/prompts"), 1);
2054
2360
  var import_picocolors7 = __toESM(require("picocolors"), 1);
2055
2361
 
2056
2362
  // src/core/config-diff.ts
@@ -2067,7 +2373,7 @@ function reconstructServerEntry(lockEntry) {
2067
2373
  }
2068
2374
  return entry;
2069
2375
  }
2070
- function computeDiff(lockfile, clientConfigs) {
2376
+ function computeDiff(lockfile, clientConfigs, options = {}) {
2071
2377
  const actions = [];
2072
2378
  for (const [server, lockEntry] of Object.entries(lockfile.servers)) {
2073
2379
  for (const client of lockEntry.clients) {
@@ -2085,19 +2391,21 @@ function computeDiff(lockfile, clientConfigs) {
2085
2391
  }
2086
2392
  }
2087
2393
  }
2394
+ const extraAction = options.remove ? "remove" : "extra";
2088
2395
  for (const [client, config] of clientConfigs) {
2089
2396
  for (const server of Object.keys(config.servers)) {
2090
2397
  if (!(server in lockfile.servers)) {
2091
- actions.push({ server, client, action: "extra" });
2398
+ actions.push({ server, client, action: extraAction });
2092
2399
  }
2093
2400
  }
2094
2401
  }
2095
2402
  return actions;
2096
2403
  }
2097
- function computeDiffFromClient(sourceClient, clientConfigs) {
2404
+ function computeDiffFromClient(sourceClient, clientConfigs, options = {}) {
2098
2405
  const actions = [];
2099
2406
  const sourceConfig = clientConfigs.get(sourceClient);
2100
2407
  if (!sourceConfig) return [];
2408
+ const extraAction = options.remove ? "remove" : "extra";
2101
2409
  for (const [client, config] of clientConfigs) {
2102
2410
  if (client === sourceClient) continue;
2103
2411
  for (const [server, entry] of Object.entries(sourceConfig.servers)) {
@@ -2109,7 +2417,7 @@ function computeDiffFromClient(sourceClient, clientConfigs) {
2109
2417
  }
2110
2418
  for (const server of Object.keys(config.servers)) {
2111
2419
  if (!(server in sourceConfig.servers)) {
2112
- actions.push({ server, client, action: "extra" });
2420
+ actions.push({ server, client, action: extraAction });
2113
2421
  }
2114
2422
  }
2115
2423
  }
@@ -2120,7 +2428,7 @@ function computeDiffFromClient(sourceClient, clientConfigs) {
2120
2428
  init_cjs_shims();
2121
2429
  init_client_detector();
2122
2430
  async function applySyncActions(actions, clients) {
2123
- const result = { applied: 0, failed: 0, errors: [] };
2431
+ const result = { applied: 0, removed: 0, failed: 0, errors: [] };
2124
2432
  const addActions = actions.filter((a) => a.action === "add" && a.entry);
2125
2433
  for (const action of addActions) {
2126
2434
  const handler = clients.get(action.client);
@@ -2145,6 +2453,30 @@ async function applySyncActions(actions, clients) {
2145
2453
  });
2146
2454
  }
2147
2455
  }
2456
+ const removeActions = actions.filter((a) => a.action === "remove");
2457
+ for (const action of removeActions) {
2458
+ const handler = clients.get(action.client);
2459
+ if (!handler) {
2460
+ result.failed++;
2461
+ result.errors.push({
2462
+ server: action.server,
2463
+ client: action.client,
2464
+ error: "No handler available for client"
2465
+ });
2466
+ continue;
2467
+ }
2468
+ try {
2469
+ await handler.removeServer(action.server);
2470
+ result.removed++;
2471
+ } catch (err) {
2472
+ result.failed++;
2473
+ result.errors.push({
2474
+ server: action.server,
2475
+ client: action.client,
2476
+ error: String(err)
2477
+ });
2478
+ }
2479
+ }
2148
2480
  return result;
2149
2481
  }
2150
2482
  async function getClientConfigs() {
@@ -2189,6 +2521,11 @@ var sync_default = (0, import_citty8.defineCommand)({
2189
2521
  description: "Preview changes without applying them",
2190
2522
  default: false
2191
2523
  },
2524
+ remove: {
2525
+ type: "boolean",
2526
+ description: "Remove extra servers not in lockfile",
2527
+ default: false
2528
+ },
2192
2529
  source: {
2193
2530
  type: "string",
2194
2531
  description: "Use a specific client as source of truth (claude-desktop, cursor, vscode, windsurf)"
@@ -2200,58 +2537,64 @@ var sync_default = (0, import_citty8.defineCommand)({
2200
2537
  }
2201
2538
  },
2202
2539
  async run({ args }) {
2203
- p7.intro(`${import_picocolors7.default.cyan("mcpman sync")}`);
2540
+ p9.intro(`${import_picocolors7.default.cyan("mcpman sync")}`);
2204
2541
  const sourceClient = args.source;
2205
2542
  if (sourceClient && !VALID_CLIENTS.includes(sourceClient)) {
2206
- p7.log.error(`Invalid --source "${sourceClient}". Must be one of: ${VALID_CLIENTS.join(", ")}`);
2543
+ p9.log.error(`Invalid --source "${sourceClient}". Must be one of: ${VALID_CLIENTS.join(", ")}`);
2207
2544
  process.exit(1);
2208
2545
  }
2209
- const spinner5 = p7.spinner();
2546
+ const spinner5 = p9.spinner();
2210
2547
  spinner5.start("Detecting clients and reading configs...");
2211
2548
  const { configs, handlers } = await getClientConfigs();
2212
2549
  spinner5.stop(`Found ${configs.size} client(s)`);
2213
2550
  if (configs.size === 0) {
2214
- p7.log.warn("No AI clients detected. Install Claude Desktop, Cursor, VS Code, or Windsurf first.");
2551
+ p9.log.warn("No AI clients detected. Install Claude Desktop, Cursor, VS Code, or Windsurf first.");
2215
2552
  process.exit(0);
2216
2553
  }
2554
+ const diffOptions = { remove: args.remove };
2217
2555
  let actions;
2218
2556
  if (sourceClient) {
2219
2557
  if (!configs.has(sourceClient)) {
2220
- p7.log.error(`Source client "${sourceClient}" is not detected or its config is unreadable.`);
2558
+ p9.log.error(`Source client "${sourceClient}" is not detected or its config is unreadable.`);
2221
2559
  process.exit(1);
2222
2560
  }
2223
- p7.log.info(`Using ${CLIENT_DISPLAY3[sourceClient]} as source of truth`);
2224
- actions = computeDiffFromClient(sourceClient, configs);
2561
+ p9.log.info(`Using ${CLIENT_DISPLAY3[sourceClient]} as source of truth`);
2562
+ actions = computeDiffFromClient(sourceClient, configs, diffOptions);
2225
2563
  } else {
2226
2564
  const lockfile = readLockfile();
2227
- actions = computeDiff(lockfile, configs);
2565
+ actions = computeDiff(lockfile, configs, diffOptions);
2228
2566
  }
2229
2567
  printDiffTable(actions);
2230
2568
  const addCount = actions.filter((a) => a.action === "add").length;
2231
2569
  const extraCount = actions.filter((a) => a.action === "extra").length;
2232
- if (addCount === 0 && extraCount === 0) {
2233
- p7.outro(import_picocolors7.default.green("All clients are in sync."));
2570
+ const removeCount = actions.filter((a) => a.action === "remove").length;
2571
+ if (addCount === 0 && removeCount === 0 && extraCount === 0) {
2572
+ p9.outro(import_picocolors7.default.green("All clients are in sync."));
2234
2573
  process.exit(0);
2235
2574
  }
2236
2575
  const parts = [];
2237
2576
  if (addCount > 0) parts.push(import_picocolors7.default.green(`${addCount} to add`));
2577
+ if (removeCount > 0) parts.push(import_picocolors7.default.red(`${removeCount} to remove`));
2238
2578
  if (extraCount > 0) parts.push(import_picocolors7.default.yellow(`${extraCount} extra (informational)`));
2239
- p7.log.info(parts.join(" \xB7 "));
2579
+ p9.log.info(parts.join(" \xB7 "));
2240
2580
  if (args["dry-run"]) {
2241
- p7.outro(import_picocolors7.default.dim("Dry run \u2014 no changes applied."));
2581
+ p9.outro(import_picocolors7.default.dim("Dry run \u2014 no changes applied."));
2242
2582
  process.exit(1);
2243
2583
  }
2244
- if (addCount === 0) {
2245
- p7.outro(import_picocolors7.default.dim("No additions needed. Extra servers left untouched."));
2584
+ if (addCount === 0 && removeCount === 0) {
2585
+ p9.outro(import_picocolors7.default.dim("No additions needed. Extra servers left untouched."));
2246
2586
  process.exit(1);
2247
2587
  }
2248
2588
  if (!args.yes) {
2249
- const confirmed = await p7.confirm({
2250
- message: `Apply ${addCount} addition(s) to client configs?`,
2589
+ const actionParts = [];
2590
+ if (addCount > 0) actionParts.push(`${addCount} addition(s)`);
2591
+ if (removeCount > 0) actionParts.push(`${removeCount} removal(s)`);
2592
+ const confirmed = await p9.confirm({
2593
+ message: `Apply ${actionParts.join(" and ")} to client configs?`,
2251
2594
  initialValue: true
2252
2595
  });
2253
- if (p7.isCancel(confirmed) || !confirmed) {
2254
- p7.outro(import_picocolors7.default.dim("Cancelled \u2014 no changes applied."));
2596
+ if (p9.isCancel(confirmed) || !confirmed) {
2597
+ p9.outro(import_picocolors7.default.dim("Cancelled \u2014 no changes applied."));
2255
2598
  process.exit(0);
2256
2599
  }
2257
2600
  }
@@ -2259,20 +2602,23 @@ var sync_default = (0, import_citty8.defineCommand)({
2259
2602
  const result = await applySyncActions(actions, handlers);
2260
2603
  spinner5.stop("Done");
2261
2604
  if (result.applied > 0) {
2262
- p7.log.success(`Added ${result.applied} server(s) to client configs.`);
2605
+ p9.log.success(`Added ${result.applied} server(s) to client configs.`);
2606
+ }
2607
+ if (result.removed > 0) {
2608
+ p9.log.success(`Removed ${result.removed} server(s) from client configs.`);
2263
2609
  }
2264
2610
  if (result.failed > 0) {
2265
2611
  for (const e of result.errors) {
2266
- p7.log.error(`Failed to add "${e.server}" to ${e.client}: ${e.error}`);
2612
+ p9.log.error(`Failed to sync "${e.server}" on ${e.client}: ${e.error}`);
2267
2613
  }
2268
2614
  }
2269
- p7.outro(result.failed === 0 ? import_picocolors7.default.green("Sync complete.") : import_picocolors7.default.yellow("Sync complete with errors."));
2615
+ p9.outro(result.failed === 0 ? import_picocolors7.default.green("Sync complete.") : import_picocolors7.default.yellow("Sync complete with errors."));
2270
2616
  process.exit(result.failed > 0 ? 1 : 0);
2271
2617
  }
2272
2618
  });
2273
2619
  function printDiffTable(actions) {
2274
2620
  if (actions.length === 0) {
2275
- p7.log.info("No actions to display.");
2621
+ p9.log.info("No actions to display.");
2276
2622
  return;
2277
2623
  }
2278
2624
  const nameWidth = Math.max(6, ...actions.map((a) => a.server.length));
@@ -2293,6 +2639,8 @@ function formatAction(action) {
2293
2639
  return [import_picocolors7.default.green("+"), import_picocolors7.default.green("missing \u2014 will add")];
2294
2640
  case "extra":
2295
2641
  return [import_picocolors7.default.yellow("?"), import_picocolors7.default.yellow("extra (not in lockfile)")];
2642
+ case "remove":
2643
+ return [import_picocolors7.default.red("\u2013"), import_picocolors7.default.red("extra \u2014 will remove")];
2296
2644
  case "ok":
2297
2645
  return [import_picocolors7.default.dim("\xB7"), import_picocolors7.default.dim("in sync")];
2298
2646
  }
@@ -2304,130 +2652,9 @@ function pad2(s, width) {
2304
2652
  // src/commands/update.ts
2305
2653
  init_cjs_shims();
2306
2654
  var import_citty9 = require("citty");
2307
- var p8 = __toESM(require("@clack/prompts"), 1);
2655
+ var p10 = __toESM(require("@clack/prompts"), 1);
2308
2656
  var import_picocolors9 = __toESM(require("picocolors"), 1);
2309
2657
 
2310
- // src/core/version-checker.ts
2311
- init_cjs_shims();
2312
- function compareVersions(a, b) {
2313
- const aParts = a.replace(/^v/, "").split(".").map(Number);
2314
- const bParts = b.replace(/^v/, "").split(".").map(Number);
2315
- const len = Math.max(aParts.length, bParts.length);
2316
- for (let i = 0; i < len; i++) {
2317
- const aN = aParts[i] ?? 0;
2318
- const bN = bParts[i] ?? 0;
2319
- if (Number.isNaN(aN) || Number.isNaN(bN)) return 0;
2320
- if (aN < bN) return -1;
2321
- if (aN > bN) return 1;
2322
- }
2323
- return 0;
2324
- }
2325
- function detectUpdateType(current, latest) {
2326
- const cParts = current.replace(/^v/, "").split(".").map(Number);
2327
- const lParts = latest.replace(/^v/, "").split(".").map(Number);
2328
- if ((lParts[0] ?? 0) > (cParts[0] ?? 0)) return "major";
2329
- if ((lParts[1] ?? 0) > (cParts[1] ?? 0)) return "minor";
2330
- return "patch";
2331
- }
2332
- async function fetchNpmLatest(packageName) {
2333
- try {
2334
- const res = await fetch(
2335
- `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`,
2336
- {
2337
- headers: { Accept: "application/json" },
2338
- signal: AbortSignal.timeout(8e3)
2339
- }
2340
- );
2341
- if (!res.ok) return null;
2342
- const data = await res.json();
2343
- return typeof data.version === "string" ? data.version : null;
2344
- } catch {
2345
- return null;
2346
- }
2347
- }
2348
- async function fetchSmitheryLatest(name) {
2349
- try {
2350
- const res = await fetch(
2351
- `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`,
2352
- {
2353
- headers: { Accept: "application/json" },
2354
- signal: AbortSignal.timeout(8e3)
2355
- }
2356
- );
2357
- if (!res.ok) return null;
2358
- const data = await res.json();
2359
- return typeof data.version === "string" ? data.version : null;
2360
- } catch {
2361
- return null;
2362
- }
2363
- }
2364
- async function fetchGithubLatest(resolved) {
2365
- const match = resolved.match(/github\.com\/([^/]+)\/([^/]+)/);
2366
- if (!match) return null;
2367
- const [, owner, repo] = match;
2368
- try {
2369
- const res = await fetch(
2370
- `https://api.github.com/repos/${owner}/${repo}/releases/latest`,
2371
- {
2372
- headers: { Accept: "application/json" },
2373
- signal: AbortSignal.timeout(8e3)
2374
- }
2375
- );
2376
- if (!res.ok) return null;
2377
- const data = await res.json();
2378
- return typeof data.tag_name === "string" ? data.tag_name.replace(/^v/, "") : null;
2379
- } catch {
2380
- return null;
2381
- }
2382
- }
2383
- async function checkVersion(name, lockEntry) {
2384
- const current = lockEntry.version;
2385
- let latest = null;
2386
- if (lockEntry.source === "npm") {
2387
- latest = await fetchNpmLatest(name);
2388
- } else if (lockEntry.source === "smithery") {
2389
- latest = await fetchSmitheryLatest(name);
2390
- } else if (lockEntry.source === "github") {
2391
- latest = await fetchGithubLatest(lockEntry.resolved);
2392
- }
2393
- if (!latest || latest === current) {
2394
- return {
2395
- server: name,
2396
- source: lockEntry.source,
2397
- currentVersion: current,
2398
- latestVersion: latest ?? current,
2399
- hasUpdate: false
2400
- };
2401
- }
2402
- const hasUpdate = compareVersions(current, latest) === -1;
2403
- return {
2404
- server: name,
2405
- source: lockEntry.source,
2406
- currentVersion: current,
2407
- latestVersion: latest,
2408
- hasUpdate,
2409
- updateType: hasUpdate ? detectUpdateType(current, latest) : void 0
2410
- };
2411
- }
2412
- async function checkAllVersions(lockfile) {
2413
- const entries = Object.entries(lockfile.servers);
2414
- if (entries.length === 0) return [];
2415
- const results = [];
2416
- const executing = /* @__PURE__ */ new Set();
2417
- for (const [name, entry] of entries) {
2418
- const p9 = checkVersion(name, entry).then((r) => {
2419
- results.push(r);
2420
- executing.delete(p9);
2421
- });
2422
- executing.add(p9);
2423
- if (executing.size >= 5) {
2424
- await Promise.race(executing);
2425
- }
2426
- }
2427
- await Promise.all(executing);
2428
- return results;
2429
- }
2430
-
2431
2658
  // src/core/update-notifier.ts
2432
2659
  init_cjs_shims();
2433
2660
  var import_node_fs5 = __toESM(require("fs"), 1);
@@ -2448,7 +2675,7 @@ function writeUpdateCache(data) {
2448
2675
  }
2449
2676
 
2450
2677
  // src/commands/update.ts
2451
- async function loadClients2() {
2678
+ async function loadClients3() {
2452
2679
  try {
2453
2680
  const mod = await Promise.resolve().then(() => (init_client_detector(), client_detector_exports));
2454
2681
  return mod.getInstalledClients();
@@ -2516,7 +2743,7 @@ var update_default = (0, import_citty9.defineCommand)({
2516
2743
  }
2517
2744
  process.exit(1);
2518
2745
  }
2519
- const spinner5 = p8.spinner();
2746
+ const spinner5 = p10.spinner();
2520
2747
  spinner5.start("Checking versions...");
2521
2748
  let updates;
2522
2749
  try {
@@ -2546,63 +2773,43 @@ var update_default = (0, import_citty9.defineCommand)({
2546
2773
  return;
2547
2774
  }
2548
2775
  if (!args.yes) {
2549
- const confirmed = await p8.confirm({
2776
+ const confirmed = await p10.confirm({
2550
2777
  message: `Apply ${outdated.length} update(s)?`,
2551
2778
  initialValue: true
2552
2779
  });
2553
- if (p8.isCancel(confirmed) || !confirmed) {
2554
- p8.outro("Cancelled.");
2780
+ if (p10.isCancel(confirmed) || !confirmed) {
2781
+ p10.outro("Cancelled.");
2555
2782
  return;
2556
2783
  }
2557
2784
  }
2558
- const clients = await loadClients2();
2785
+ const clients = await loadClients3();
2559
2786
  let successCount = 0;
2560
2787
  for (const update of outdated) {
2561
- const lockEntry = servers[update.server];
2562
- const input = lockEntry.source === "smithery" ? `smithery:${update.server}` : lockEntry.source === "github" ? lockEntry.resolved : update.server;
2563
- const s = p8.spinner();
2788
+ const s = p10.spinner();
2564
2789
  s.start(`Updating ${update.server}...`);
2565
- try {
2566
- const metadata = await resolveServer(input);
2567
- const integrity = computeIntegrity(metadata.resolved);
2568
- addEntry(update.server, {
2569
- ...lockEntry,
2570
- version: metadata.version,
2571
- resolved: metadata.resolved,
2572
- integrity,
2573
- command: metadata.command,
2574
- args: metadata.args,
2575
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
2576
- });
2577
- const entryClients = clients.filter(
2578
- (c) => lockEntry.clients.includes(c.type)
2579
- );
2580
- for (const client of entryClients) {
2581
- try {
2582
- await client.addServer(update.server, {
2583
- command: metadata.command,
2584
- args: metadata.args
2585
- });
2586
- } catch {
2587
- }
2588
- }
2589
- s.stop(`${import_picocolors9.default.green("\u2713")} ${update.server}: ${update.currentVersion} \u2192 ${metadata.version}`);
2790
+ const result = await applyServerUpdate(
2791
+ update.server,
2792
+ servers[update.server],
2793
+ clients
2794
+ );
2795
+ if (result.success) {
2796
+ s.stop(`${import_picocolors9.default.green("\u2713")} ${update.server}: ${result.fromVersion} \u2192 ${result.toVersion}`);
2590
2797
  successCount++;
2591
- } catch (err) {
2592
- s.stop(`${import_picocolors9.default.red("\u2717")} ${update.server}: ${err instanceof Error ? err.message : String(err)}`);
2798
+ } else {
2799
+ s.stop(`${import_picocolors9.default.red("\u2717")} ${update.server}: ${result.error}`);
2593
2800
  }
2594
2801
  }
2595
2802
  const freshLockfile = readLockfile(resolveLockfilePath());
2596
2803
  const freshUpdates = await checkAllVersions(freshLockfile);
2597
2804
  writeUpdateCache({ lastCheck: (/* @__PURE__ */ new Date()).toISOString(), updates: freshUpdates });
2598
- p8.outro(`${successCount} of ${outdated.length} server(s) updated.`);
2805
+ p10.outro(`${successCount} of ${outdated.length} server(s) updated.`);
2599
2806
  }
2600
2807
  });
2601
2808
 
2602
2809
  // src/utils/constants.ts
2603
2810
  init_cjs_shims();
2604
2811
  var APP_NAME = "mcpman";
2605
- var APP_VERSION = "0.2.0";
2812
+ var APP_VERSION = "0.3.0";
2606
2813
  var APP_DESCRIPTION = "The package manager for MCP servers";
2607
2814
 
2608
2815
  // src/index.ts