mcpman 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/dist/index.cjs +638 -431
- package/dist/index.js +618 -417
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -159,9 +159,9 @@ async function atomicWrite(filePath, content) {
|
|
|
159
159
|
throw err;
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
|
-
async function pathExists(
|
|
162
|
+
async function pathExists(p11) {
|
|
163
163
|
try {
|
|
164
|
-
await import_node_fs3.default.promises.access(
|
|
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(
|
|
485
|
+
async function getMasterPassword(confirm8 = false) {
|
|
486
486
|
if (_cachedPassword) return _cachedPassword;
|
|
487
|
-
const password2 = await
|
|
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 (
|
|
492
|
-
|
|
491
|
+
if (p3.isCancel(password2)) {
|
|
492
|
+
p3.cancel("Vault access cancelled.");
|
|
493
493
|
process.exit(0);
|
|
494
494
|
}
|
|
495
|
-
if (
|
|
496
|
-
const confirm22 = await
|
|
497
|
-
if (
|
|
498
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
770
|
+
const p11 = scanServer(name, entry).then((r) => {
|
|
770
771
|
results.push(r);
|
|
771
|
-
executing.delete(
|
|
772
|
+
executing.delete(p11);
|
|
772
773
|
});
|
|
773
|
-
executing.add(
|
|
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: "
|
|
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
|
-
|
|
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
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
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
|
-
|
|
1376
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1713
|
+
p2.log.warn(`Lockfile already exists: ${existing} \u2014 overwriting (non-interactive).`);
|
|
1411
1714
|
} else {
|
|
1412
|
-
|
|
1413
|
-
const overwrite = await
|
|
1414
|
-
if (
|
|
1415
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1440
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
1458
|
-
|
|
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
|
-
|
|
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
|
|
1799
|
+
var p5 = __toESM(require("@clack/prompts"), 1);
|
|
1497
1800
|
|
|
1498
|
-
// src/core/
|
|
1801
|
+
// src/core/installer-vault-helpers.ts
|
|
1499
1802
|
init_cjs_shims();
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
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
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
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
|
|
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
|
-
|
|
1544
|
-
const spinner5 =
|
|
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
|
-
|
|
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
|
|
1857
|
+
const clients = await loadClients2();
|
|
1556
1858
|
if (clients.length === 0) {
|
|
1557
|
-
|
|
1558
|
-
|
|
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
|
-
|
|
1566
|
-
|
|
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
|
|
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 (
|
|
1579
|
-
|
|
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
|
|
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
|
|
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 (
|
|
1598
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1636
|
-
|
|
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
|
|
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
|
-
|
|
1712
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2161
|
+
p7.log.info(`Did you mean: ${similar.map((s) => import_picocolors5.default.cyan(s.name)).join(", ")}?`);
|
|
1856
2162
|
}
|
|
1857
|
-
|
|
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
|
-
|
|
1866
|
-
|
|
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
|
|
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 (
|
|
1882
|
-
|
|
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
|
|
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 (
|
|
1893
|
-
|
|
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
|
-
|
|
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)
|
|
1915
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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 (
|
|
2025
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
2540
|
+
p9.intro(`${import_picocolors7.default.cyan("mcpman sync")}`);
|
|
2204
2541
|
const sourceClient = args.source;
|
|
2205
2542
|
if (sourceClient && !VALID_CLIENTS.includes(sourceClient)) {
|
|
2206
|
-
|
|
2543
|
+
p9.log.error(`Invalid --source "${sourceClient}". Must be one of: ${VALID_CLIENTS.join(", ")}`);
|
|
2207
2544
|
process.exit(1);
|
|
2208
2545
|
}
|
|
2209
|
-
const spinner5 =
|
|
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
|
-
|
|
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
|
-
|
|
2558
|
+
p9.log.error(`Source client "${sourceClient}" is not detected or its config is unreadable.`);
|
|
2221
2559
|
process.exit(1);
|
|
2222
2560
|
}
|
|
2223
|
-
|
|
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
|
-
|
|
2233
|
-
|
|
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
|
-
|
|
2579
|
+
p9.log.info(parts.join(" \xB7 "));
|
|
2240
2580
|
if (args["dry-run"]) {
|
|
2241
|
-
|
|
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
|
-
|
|
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
|
|
2250
|
-
|
|
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 (
|
|
2254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2612
|
+
p9.log.error(`Failed to sync "${e.server}" on ${e.client}: ${e.error}`);
|
|
2267
2613
|
}
|
|
2268
2614
|
}
|
|
2269
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
2776
|
+
const confirmed = await p10.confirm({
|
|
2550
2777
|
message: `Apply ${outdated.length} update(s)?`,
|
|
2551
2778
|
initialValue: true
|
|
2552
2779
|
});
|
|
2553
|
-
if (
|
|
2554
|
-
|
|
2780
|
+
if (p10.isCancel(confirmed) || !confirmed) {
|
|
2781
|
+
p10.outro("Cancelled.");
|
|
2555
2782
|
return;
|
|
2556
2783
|
}
|
|
2557
2784
|
}
|
|
2558
|
-
const clients = await
|
|
2785
|
+
const clients = await loadClients3();
|
|
2559
2786
|
let successCount = 0;
|
|
2560
2787
|
for (const update of outdated) {
|
|
2561
|
-
const
|
|
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
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
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
|
-
}
|
|
2592
|
-
s.stop(`${import_picocolors9.default.red("\u2717")} ${update.server}: ${
|
|
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
|
-
|
|
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.
|
|
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
|