mcpman 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,19 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ computeTrustScore
4
+ } from "./chunk-RGKHLY5G.js";
5
+ import {
6
+ getConfigPath,
3
7
  getInstalledClients
4
- } from "./chunk-QY22QTBR.js";
8
+ } from "./chunk-RMMEBP2J.js";
5
9
  import {
6
10
  getMasterPassword,
11
+ getSecretsForServer,
7
12
  listSecrets,
8
13
  removeSecret,
9
14
  setSecret
10
15
  } from "./chunk-6X6Q6UZC.js";
11
16
 
12
17
  // src/index.ts
13
- import { defineCommand as defineCommand10, runMain } from "citty";
18
+ import { defineCommand as defineCommand14, runMain } from "citty";
14
19
 
15
20
  // src/commands/audit.ts
16
21
  import { defineCommand } from "citty";
22
+ import * as p from "@clack/prompts";
17
23
  import pc from "picocolors";
18
24
  import { createSpinner } from "nanospinner";
19
25
 
@@ -191,8 +197,8 @@ async function scanServer(name, entry) {
191
197
  fetchNpmMetadata(name),
192
198
  fetchVulnerabilities(name, entry.version)
193
199
  ]);
194
- const { computeTrustScore } = await import("./trust-scorer-LYC6KZCD.js");
195
- const { score, riskLevel } = computeTrustScore(metadata, vulnerabilities);
200
+ const { computeTrustScore: computeTrustScore2 } = await import("./trust-scorer-G74WK25Q.js");
201
+ const { score, riskLevel } = computeTrustScore2(metadata, vulnerabilities);
196
202
  const report = {
197
203
  server: name,
198
204
  source: "npm",
@@ -209,17 +215,324 @@ async function scanAllServers(servers, concurrency = 3) {
209
215
  const results = [];
210
216
  const executing = /* @__PURE__ */ new Set();
211
217
  for (const [name, entry] of entries) {
212
- const p8 = scanServer(name, entry).then((r) => {
218
+ const p11 = scanServer(name, entry).then((r) => {
213
219
  results.push(r);
214
- executing.delete(p8);
220
+ executing.delete(p11);
215
221
  });
216
- executing.add(p8);
222
+ executing.add(p11);
217
223
  if (executing.size >= concurrency) await Promise.race(executing);
218
224
  }
219
225
  await Promise.all(executing);
220
226
  return results;
221
227
  }
222
228
 
229
+ // src/core/version-checker.ts
230
+ function compareVersions(a, b) {
231
+ const aParts = a.replace(/^v/, "").split(".").map(Number);
232
+ const bParts = b.replace(/^v/, "").split(".").map(Number);
233
+ const len = Math.max(aParts.length, bParts.length);
234
+ for (let i = 0; i < len; i++) {
235
+ const aN = aParts[i] ?? 0;
236
+ const bN = bParts[i] ?? 0;
237
+ if (Number.isNaN(aN) || Number.isNaN(bN)) return 0;
238
+ if (aN < bN) return -1;
239
+ if (aN > bN) return 1;
240
+ }
241
+ return 0;
242
+ }
243
+ function detectUpdateType(current, latest) {
244
+ const cParts = current.replace(/^v/, "").split(".").map(Number);
245
+ const lParts = latest.replace(/^v/, "").split(".").map(Number);
246
+ if ((lParts[0] ?? 0) > (cParts[0] ?? 0)) return "major";
247
+ if ((lParts[1] ?? 0) > (cParts[1] ?? 0)) return "minor";
248
+ return "patch";
249
+ }
250
+ async function fetchNpmLatest(packageName) {
251
+ try {
252
+ const res = await fetch(
253
+ `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`,
254
+ {
255
+ headers: { Accept: "application/json" },
256
+ signal: AbortSignal.timeout(8e3)
257
+ }
258
+ );
259
+ if (!res.ok) return null;
260
+ const data = await res.json();
261
+ return typeof data.version === "string" ? data.version : null;
262
+ } catch {
263
+ return null;
264
+ }
265
+ }
266
+ async function fetchSmitheryLatest(name) {
267
+ try {
268
+ const res = await fetch(
269
+ `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`,
270
+ {
271
+ headers: { Accept: "application/json" },
272
+ signal: AbortSignal.timeout(8e3)
273
+ }
274
+ );
275
+ if (!res.ok) return null;
276
+ const data = await res.json();
277
+ return typeof data.version === "string" ? data.version : null;
278
+ } catch {
279
+ return null;
280
+ }
281
+ }
282
+ async function fetchGithubLatest(resolved) {
283
+ const match = resolved.match(/github\.com\/([^/]+)\/([^/]+)/);
284
+ if (!match) return null;
285
+ const [, owner, repo] = match;
286
+ try {
287
+ const res = await fetch(
288
+ `https://api.github.com/repos/${owner}/${repo}/releases/latest`,
289
+ {
290
+ headers: { Accept: "application/json" },
291
+ signal: AbortSignal.timeout(8e3)
292
+ }
293
+ );
294
+ if (!res.ok) return null;
295
+ const data = await res.json();
296
+ return typeof data.tag_name === "string" ? data.tag_name.replace(/^v/, "") : null;
297
+ } catch {
298
+ return null;
299
+ }
300
+ }
301
+ async function checkVersion(name, lockEntry) {
302
+ const current = lockEntry.version;
303
+ let latest = null;
304
+ if (lockEntry.source === "npm") {
305
+ latest = await fetchNpmLatest(name);
306
+ } else if (lockEntry.source === "smithery") {
307
+ latest = await fetchSmitheryLatest(name);
308
+ } else if (lockEntry.source === "github") {
309
+ latest = await fetchGithubLatest(lockEntry.resolved);
310
+ }
311
+ if (!latest || latest === current) {
312
+ return {
313
+ server: name,
314
+ source: lockEntry.source,
315
+ currentVersion: current,
316
+ latestVersion: latest ?? current,
317
+ hasUpdate: false
318
+ };
319
+ }
320
+ const hasUpdate = compareVersions(current, latest) === -1;
321
+ return {
322
+ server: name,
323
+ source: lockEntry.source,
324
+ currentVersion: current,
325
+ latestVersion: latest,
326
+ hasUpdate,
327
+ updateType: hasUpdate ? detectUpdateType(current, latest) : void 0
328
+ };
329
+ }
330
+ async function checkAllVersions(lockfile) {
331
+ const entries = Object.entries(lockfile.servers);
332
+ if (entries.length === 0) return [];
333
+ const results = [];
334
+ const executing = /* @__PURE__ */ new Set();
335
+ for (const [name, entry] of entries) {
336
+ const p11 = checkVersion(name, entry).then((r) => {
337
+ results.push(r);
338
+ executing.delete(p11);
339
+ });
340
+ executing.add(p11);
341
+ if (executing.size >= 5) {
342
+ await Promise.race(executing);
343
+ }
344
+ }
345
+ await Promise.all(executing);
346
+ return results;
347
+ }
348
+
349
+ // src/core/registry.ts
350
+ import { createHash } from "crypto";
351
+ function computeIntegrity(resolvedUrl) {
352
+ const hash = createHash("sha512").update(resolvedUrl).digest("base64");
353
+ return `sha512-${hash}`;
354
+ }
355
+ async function resolveFromSmithery(name) {
356
+ const url = `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`;
357
+ let data;
358
+ try {
359
+ const res = await fetch(url, {
360
+ headers: { Accept: "application/json" },
361
+ signal: AbortSignal.timeout(8e3)
362
+ });
363
+ if (res.status === 404) {
364
+ throw new Error(`Server '${name}' not found on Smithery registry`);
365
+ }
366
+ if (!res.ok) {
367
+ throw new Error(`Smithery API error: ${res.status}`);
368
+ }
369
+ data = await res.json();
370
+ } catch (err) {
371
+ if (err instanceof Error && err.message.includes("not found")) throw err;
372
+ throw new Error(
373
+ `Cannot reach Smithery registry: ${err instanceof Error ? err.message : String(err)}`
374
+ );
375
+ }
376
+ const version = typeof data.version === "string" ? data.version : "latest";
377
+ const command = typeof data.command === "string" ? data.command : "npx";
378
+ const args = Array.isArray(data.args) ? data.args : ["-y", `${name}@${version}`];
379
+ const envVars = Array.isArray(data.envVars) ? data.envVars : [];
380
+ const resolved = typeof data.resolved === "string" ? data.resolved : `smithery:${name}@${version}`;
381
+ return {
382
+ name,
383
+ version,
384
+ description: typeof data.description === "string" ? data.description : "",
385
+ runtime: "node",
386
+ command,
387
+ args,
388
+ envVars,
389
+ resolved
390
+ };
391
+ }
392
+ async function resolveFromNpm(packageName) {
393
+ const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
394
+ let data;
395
+ try {
396
+ const res = await fetch(url, {
397
+ headers: { Accept: "application/json" },
398
+ signal: AbortSignal.timeout(8e3)
399
+ });
400
+ if (res.status === 404) {
401
+ throw new Error(`Package '${packageName}' not found on npm`);
402
+ }
403
+ if (!res.ok) {
404
+ throw new Error(`npm registry error: ${res.status}`);
405
+ }
406
+ data = await res.json();
407
+ } catch (err) {
408
+ if (err instanceof Error && err.message.includes("not found")) throw err;
409
+ throw new Error(
410
+ `Cannot reach npm registry: ${err instanceof Error ? err.message : String(err)}`
411
+ );
412
+ }
413
+ const version = typeof data.version === "string" ? data.version : "latest";
414
+ const resolved = `https://registry.npmjs.org/${packageName}/-/${packageName.replace(/^@[^/]+\//, "")}-${version}.tgz`;
415
+ const mcpField = data.mcp && typeof data.mcp === "object" ? data.mcp : null;
416
+ const envVars = mcpField?.envVars ? mcpField.envVars : [];
417
+ return {
418
+ name: packageName,
419
+ version,
420
+ description: typeof data.description === "string" ? data.description : "",
421
+ runtime: "node",
422
+ command: "npx",
423
+ args: ["-y", `${packageName}@${version}`],
424
+ envVars,
425
+ resolved
426
+ };
427
+ }
428
+ async function resolveFromGitHub(githubUrl) {
429
+ const match = githubUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
430
+ if (!match) {
431
+ throw new Error(`Invalid GitHub URL: ${githubUrl}`);
432
+ }
433
+ const [, owner, repo] = match;
434
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/package.json`;
435
+ let pkgData = {};
436
+ try {
437
+ const res = await fetch(rawUrl, { signal: AbortSignal.timeout(8e3) });
438
+ if (res.ok) {
439
+ pkgData = await res.json();
440
+ }
441
+ } catch {
442
+ }
443
+ const version = typeof pkgData.version === "string" ? pkgData.version : "main";
444
+ const name = typeof pkgData.name === "string" ? pkgData.name : `${owner}/${repo}`;
445
+ return {
446
+ name,
447
+ version,
448
+ description: typeof pkgData.description === "string" ? pkgData.description : "",
449
+ runtime: "node",
450
+ command: "npx",
451
+ args: ["-y", githubUrl],
452
+ envVars: [],
453
+ resolved: githubUrl
454
+ };
455
+ }
456
+
457
+ // src/core/server-resolver.ts
458
+ function detectSource(input) {
459
+ if (input.startsWith("smithery:")) {
460
+ return { type: "smithery", input: input.slice(9) };
461
+ }
462
+ if (input.startsWith("https://github.com/") || input.startsWith("github.com/")) {
463
+ return { type: "github", input };
464
+ }
465
+ return { type: "npm", input };
466
+ }
467
+ function parseEnvFlags(envFlags) {
468
+ if (!envFlags) return {};
469
+ const flags = Array.isArray(envFlags) ? envFlags : [envFlags];
470
+ const result = {};
471
+ for (const flag of flags) {
472
+ const idx = flag.indexOf("=");
473
+ if (idx > 0) {
474
+ result[flag.slice(0, idx)] = flag.slice(idx + 1);
475
+ }
476
+ }
477
+ return result;
478
+ }
479
+ async function resolveServer(input) {
480
+ const source = detectSource(input);
481
+ switch (source.type) {
482
+ case "smithery":
483
+ return resolveFromSmithery(source.input);
484
+ case "github":
485
+ return resolveFromGitHub(source.input);
486
+ case "npm":
487
+ return resolveFromNpm(source.input);
488
+ }
489
+ }
490
+
491
+ // src/core/server-updater.ts
492
+ async function applyServerUpdate(serverName, lockEntry, clients) {
493
+ const fromVersion = lockEntry.version;
494
+ const input = lockEntry.source === "smithery" ? `smithery:${serverName}` : lockEntry.source === "github" ? lockEntry.resolved : serverName;
495
+ try {
496
+ const metadata = await resolveServer(input);
497
+ const integrity = computeIntegrity(metadata.resolved);
498
+ addEntry(serverName, {
499
+ ...lockEntry,
500
+ version: metadata.version,
501
+ resolved: metadata.resolved,
502
+ integrity,
503
+ command: metadata.command,
504
+ args: metadata.args,
505
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
506
+ });
507
+ const targetClients = clients.filter(
508
+ (c) => lockEntry.clients.includes(c.type)
509
+ );
510
+ for (const client of targetClients) {
511
+ try {
512
+ await client.addServer(serverName, {
513
+ command: metadata.command,
514
+ args: metadata.args
515
+ });
516
+ } catch {
517
+ }
518
+ }
519
+ return {
520
+ server: serverName,
521
+ success: true,
522
+ fromVersion,
523
+ toVersion: metadata.version
524
+ };
525
+ } catch (err) {
526
+ return {
527
+ server: serverName,
528
+ success: false,
529
+ fromVersion,
530
+ toVersion: fromVersion,
531
+ error: err instanceof Error ? err.message : String(err)
532
+ };
533
+ }
534
+ }
535
+
223
536
  // src/commands/audit.ts
224
537
  function colorRisk(level, score) {
225
538
  const label = score !== null ? `${score}/100 (${level})` : level;
@@ -290,7 +603,12 @@ var audit_default = defineCommand({
290
603
  },
291
604
  fix: {
292
605
  type: "boolean",
293
- description: "Show available fix versions for vulnerable packages",
606
+ description: "Apply updates to fix vulnerable packages",
607
+ default: false
608
+ },
609
+ yes: {
610
+ type: "boolean",
611
+ description: "Skip confirmation prompt (use with --fix)",
294
612
  default: false
295
613
  }
296
614
  },
@@ -342,30 +660,270 @@ var audit_default = defineCommand({
342
660
  if (npmReports.length < reports.length) {
343
661
  parts.push(pc.dim(`${reports.length - npmReports.length} non-npm (unverified)`));
344
662
  }
345
- if (withIssues.length > 0) {
346
- parts.push(pc.yellow(`${withIssues.length} with issues`));
663
+ if (withIssues.length > 0) {
664
+ parts.push(pc.yellow(`${withIssues.length} with issues`));
665
+ } else {
666
+ parts.push(pc.green("all clear"));
667
+ }
668
+ console.log(`
669
+ Summary: ${parts.join(" | ")}
670
+ `);
671
+ if (args.fix) {
672
+ await runAuditFix(reports, lockfile.servers, args.yes);
673
+ }
674
+ }
675
+ });
676
+ async function loadClients() {
677
+ try {
678
+ const mod = await import("./client-detector-UAP2EYZA.js");
679
+ return mod.getInstalledClients();
680
+ } catch {
681
+ return [];
682
+ }
683
+ }
684
+ async function runAuditFix(reports, servers, skipConfirm) {
685
+ const npmWithVulns = reports.filter(
686
+ (r) => r.vulnerabilities.length > 0 && r.source === "npm"
687
+ );
688
+ const nonNpmWithVulns = reports.filter(
689
+ (r) => r.vulnerabilities.length > 0 && r.source !== "npm"
690
+ );
691
+ if (nonNpmWithVulns.length > 0) {
692
+ console.log(pc.yellow(" Non-npm servers require manual update:"));
693
+ for (const r of nonNpmWithVulns) {
694
+ console.log(` ${pc.dim("\u2192")} ${r.server} (${r.source})`);
695
+ }
696
+ console.log();
697
+ }
698
+ if (npmWithVulns.length === 0) {
699
+ console.log(pc.green(" No fixable vulnerabilities found.\n"));
700
+ return;
701
+ }
702
+ const versionSpinner = createSpinner("Checking for available updates...").start();
703
+ const versionChecks = await Promise.all(
704
+ npmWithVulns.map((r) => checkVersion(r.server, servers[r.server]))
705
+ );
706
+ versionSpinner.success({ text: "Version check complete" });
707
+ const updatable = versionChecks.filter((u) => u.hasUpdate);
708
+ if (updatable.length === 0) {
709
+ console.log(pc.yellow(
710
+ " Vulnerable servers have no newer versions available yet.\n Allow time for registry to publish fixes.\n"
711
+ ));
712
+ return;
713
+ }
714
+ console.log(pc.bold(`
715
+ ${updatable.length} server(s) can be updated to fix vulnerabilities:
716
+ `));
717
+ for (const u of updatable) {
718
+ console.log(` ${pc.cyan("\u2192")} ${u.server} ${pc.dim(u.currentVersion)} \u2192 ${pc.green(u.latestVersion)}`);
719
+ }
720
+ console.log();
721
+ if (!skipConfirm) {
722
+ const confirmed = await p.confirm({
723
+ message: `Update ${updatable.length} vulnerable server(s)?`,
724
+ initialValue: true
725
+ });
726
+ if (p.isCancel(confirmed) || !confirmed) {
727
+ p.outro("Cancelled.");
728
+ return;
729
+ }
730
+ }
731
+ const clients = await loadClients();
732
+ let successCount = 0;
733
+ const results = [];
734
+ for (const u of updatable) {
735
+ const s = createSpinner(`Updating ${u.server}...`).start();
736
+ const result = await applyServerUpdate(u.server, servers[u.server], clients);
737
+ if (result.success) {
738
+ s.success({ text: `${pc.green("\u2713")} ${u.server}: ${result.fromVersion} \u2192 ${result.toVersion}` });
739
+ successCount++;
740
+ } else {
741
+ s.error({ text: `${pc.red("\u2717")} ${u.server}: ${result.error}` });
742
+ }
743
+ results.push({
744
+ server: u.server,
745
+ from: result.fromVersion,
746
+ to: result.toVersion,
747
+ ok: result.success,
748
+ error: result.error
749
+ });
750
+ }
751
+ console.log();
752
+ if (successCount > 0) {
753
+ const updatedNames = results.filter((r) => r.ok).map((r) => r.server);
754
+ const freshLockfile = readLockfile();
755
+ const rescanSpinner = createSpinner("Re-scanning updated servers...").start();
756
+ const afterReports = await Promise.all(
757
+ updatedNames.map((name) => scanServer(name, freshLockfile.servers[name]))
758
+ );
759
+ rescanSpinner.success({ text: "Re-scan complete" });
760
+ console.log(pc.bold("\n Before / After:\n"));
761
+ for (const after of afterReports) {
762
+ const before = reports.find((r) => r.server === after.server);
763
+ const beforeVulns = before?.vulnerabilities.length ?? 0;
764
+ const afterVulns = after.vulnerabilities.length;
765
+ const improved = afterVulns < beforeVulns ? pc.green("improved") : pc.yellow("unchanged");
766
+ console.log(
767
+ ` ${pc.bold(after.server)} vulns: ${beforeVulns} \u2192 ${afterVulns} [${improved}]`
768
+ );
769
+ }
770
+ console.log();
771
+ }
772
+ console.log(`
773
+ ${successCount} of ${updatable.length} server(s) updated.
774
+ `);
775
+ }
776
+
777
+ // src/commands/config.ts
778
+ import { defineCommand as defineCommand2 } from "citty";
779
+ import pc2 from "picocolors";
780
+ import * as p2 from "@clack/prompts";
781
+
782
+ // src/core/config-service.ts
783
+ import fs3 from "fs";
784
+ import path3 from "path";
785
+ var VALID_KEYS = /* @__PURE__ */ new Set([
786
+ "defaultClient",
787
+ "updateCheckInterval",
788
+ "preferredRegistry",
789
+ "vaultTimeout"
790
+ ]);
791
+ function readConfig(configPath = getConfigPath()) {
792
+ try {
793
+ const raw = fs3.readFileSync(configPath, "utf-8");
794
+ const parsed = JSON.parse(raw);
795
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
796
+ return {};
797
+ }
798
+ return parsed;
799
+ } catch {
800
+ return {};
801
+ }
802
+ }
803
+ function writeConfig(data, configPath = getConfigPath()) {
804
+ const dir = path3.dirname(configPath);
805
+ fs3.mkdirSync(dir, { recursive: true });
806
+ const tmp = `${configPath}.tmp`;
807
+ fs3.writeFileSync(tmp, JSON.stringify(data, null, 2), { encoding: "utf-8" });
808
+ fs3.renameSync(tmp, configPath);
809
+ }
810
+ function getConfigValue(key, configPath = getConfigPath()) {
811
+ const data = readConfig(configPath);
812
+ if (!VALID_KEYS.has(key)) return void 0;
813
+ return data[key];
814
+ }
815
+ function setConfigValue(key, value, configPath = getConfigPath()) {
816
+ const data = readConfig(configPath);
817
+ if (!VALID_KEYS.has(key)) {
818
+ throw new Error(`Unknown config key: "${key}". Valid keys: ${[...VALID_KEYS].join(", ")}`);
819
+ }
820
+ data[key] = value;
821
+ writeConfig(data, configPath);
822
+ }
823
+
824
+ // src/commands/config.ts
825
+ function coerceValue(raw) {
826
+ if (raw === "true") return true;
827
+ if (raw === "false") return false;
828
+ const num = Number(raw);
829
+ if (!Number.isNaN(num) && raw.trim() !== "") return num;
830
+ return raw;
831
+ }
832
+ var setCommand = defineCommand2({
833
+ meta: { name: "set", description: "Set a config value" },
834
+ args: {
835
+ key: {
836
+ type: "positional",
837
+ description: "Config key (e.g. defaultClient)",
838
+ required: true
839
+ },
840
+ value: {
841
+ type: "positional",
842
+ description: "Value to set",
843
+ required: true
844
+ }
845
+ },
846
+ run({ args }) {
847
+ try {
848
+ const coerced = coerceValue(args.value);
849
+ setConfigValue(args.key, coerced);
850
+ console.log(
851
+ `${pc2.green("\u2713")} Set ${pc2.bold(args.key)} = ${pc2.cyan(String(coerced))}`
852
+ );
853
+ } catch (err) {
854
+ console.error(`${pc2.red("\u2717")} ${String(err)}`);
855
+ process.exit(1);
856
+ }
857
+ }
858
+ });
859
+ var getCommand = defineCommand2({
860
+ meta: { name: "get", description: "Get a config value" },
861
+ args: {
862
+ key: {
863
+ type: "positional",
864
+ description: "Config key to read",
865
+ required: true
866
+ }
867
+ },
868
+ run({ args }) {
869
+ const val = getConfigValue(args.key);
870
+ if (val === void 0) {
871
+ console.log(pc2.dim(`${args.key}: (not set)`));
347
872
  } else {
348
- parts.push(pc.green("all clear"));
873
+ console.log(`${pc2.bold(args.key)}: ${pc2.cyan(String(val))}`);
349
874
  }
350
- console.log(`
351
- Summary: ${parts.join(" | ")}
352
- `);
353
- if (args.fix) {
354
- const withVulns = reports.filter((r) => r.vulnerabilities.length > 0);
355
- if (withVulns.length > 0) {
356
- console.log(pc.bold(" Fix suggestions:"));
357
- for (const r of withVulns) {
358
- console.log(` ${pc.cyan("\u2192")} Run ${pc.cyan(`mcpman install ${r.server}@latest`)} to update`);
359
- }
360
- console.log();
361
- }
875
+ }
876
+ });
877
+ var listCommand = defineCommand2({
878
+ meta: { name: "list", description: "List all config values" },
879
+ run() {
880
+ const data = readConfig();
881
+ const entries = Object.entries(data);
882
+ if (entries.length === 0) {
883
+ console.log(pc2.dim("No config values set. Use `mcpman config set <key> <value>`."));
884
+ return;
885
+ }
886
+ console.log("");
887
+ console.log(pc2.bold("mcpman config:"));
888
+ console.log("");
889
+ for (const [key, val] of entries) {
890
+ console.log(` ${pc2.green("\u25CF")} ${pc2.bold(key)} ${pc2.cyan(String(val))}`);
891
+ }
892
+ console.log("");
893
+ console.log(pc2.dim(` ${entries.length} key${entries.length !== 1 ? "s" : ""} configured`));
894
+ }
895
+ });
896
+ var resetCommand = defineCommand2({
897
+ meta: { name: "reset", description: "Reset config to defaults (removes config file)" },
898
+ async run() {
899
+ const confirmed = await p2.confirm({
900
+ message: "Reset all config values to defaults?",
901
+ initialValue: false
902
+ });
903
+ if (p2.isCancel(confirmed) || !confirmed) {
904
+ p2.cancel("Cancelled.");
905
+ return;
362
906
  }
907
+ writeConfig({});
908
+ console.log(`${pc2.green("\u2713")} Config reset to defaults.`);
909
+ }
910
+ });
911
+ var config_default = defineCommand2({
912
+ meta: {
913
+ name: "config",
914
+ description: "Manage mcpman CLI configuration"
915
+ },
916
+ subCommands: {
917
+ set: setCommand,
918
+ get: getCommand,
919
+ list: listCommand,
920
+ reset: resetCommand
363
921
  }
364
922
  });
365
923
 
366
924
  // src/commands/doctor.ts
367
- import { defineCommand as defineCommand2 } from "citty";
368
- import pc2 from "picocolors";
925
+ import { defineCommand as defineCommand3 } from "citty";
926
+ import pc3 from "picocolors";
369
927
 
370
928
  // src/core/server-inventory.ts
371
929
  async function getInstalledServers(clientFilter) {
@@ -629,12 +1187,12 @@ async function quickHealthProbe(config, timeoutMs = 3e3) {
629
1187
 
630
1188
  // src/commands/doctor.ts
631
1189
  var CHECK_ICON = {
632
- pass: pc2.green("\u2713"),
633
- fail: pc2.red("\u2717"),
634
- skip: pc2.dim("-"),
635
- warn: pc2.yellow("\u26A0")
1190
+ pass: pc3.green("\u2713"),
1191
+ fail: pc3.red("\u2717"),
1192
+ skip: pc3.dim("-"),
1193
+ warn: pc3.yellow("\u26A0")
636
1194
  };
637
- var doctor_default = defineCommand2({
1195
+ var doctor_default = defineCommand3({
638
1196
  meta: {
639
1197
  name: "doctor",
640
1198
  description: "Check MCP server health and configuration"
@@ -647,10 +1205,10 @@ var doctor_default = defineCommand2({
647
1205
  }
648
1206
  },
649
1207
  async run({ args }) {
650
- console.log(pc2.bold("\n mcpman doctor\n"));
1208
+ console.log(pc3.bold("\n mcpman doctor\n"));
651
1209
  const servers = await getInstalledServers();
652
1210
  if (servers.length === 0) {
653
- console.log(pc2.dim(" No MCP servers installed. Run mcpman install <server> to get started."));
1211
+ console.log(pc3.dim(" No MCP servers installed. Run mcpman install <server> to get started."));
654
1212
  return;
655
1213
  }
656
1214
  const tasks = servers.map((s) => () => checkServerHealth(s.name, s.config));
@@ -662,14 +1220,14 @@ var doctor_default = defineCommand2({
662
1220
  if (result.status === "healthy") passed++;
663
1221
  else failed++;
664
1222
  }
665
- console.log(pc2.dim(" " + "\u2500".repeat(50)));
1223
+ console.log(pc3.dim(" " + "\u2500".repeat(50)));
666
1224
  const parts = [];
667
- if (passed > 0) parts.push(pc2.green(`${passed} healthy`));
668
- if (failed > 0) parts.push(pc2.red(`${failed} unhealthy`));
1225
+ if (passed > 0) parts.push(pc3.green(`${passed} healthy`));
1226
+ if (failed > 0) parts.push(pc3.red(`${failed} unhealthy`));
669
1227
  console.log(` Summary: ${parts.join(", ")}`);
670
1228
  if (failed > 0) {
671
1229
  if (!args.fix) {
672
- console.log(pc2.dim(` Run ${pc2.cyan("mcpman doctor --fix")} for fix suggestions.
1230
+ console.log(pc3.dim(` Run ${pc3.cyan("mcpman doctor --fix")} for fix suggestions.
673
1231
  `));
674
1232
  }
675
1233
  process.exit(1);
@@ -678,13 +1236,13 @@ var doctor_default = defineCommand2({
678
1236
  }
679
1237
  });
680
1238
  function printServerResult(result, showFix) {
681
- const icon = result.status === "healthy" ? pc2.green("\u25CF") : pc2.red("\u25CF");
682
- console.log(` ${icon} ${pc2.bold(result.serverName)}`);
1239
+ const icon = result.status === "healthy" ? pc3.green("\u25CF") : pc3.red("\u25CF");
1240
+ console.log(` ${icon} ${pc3.bold(result.serverName)}`);
683
1241
  for (const check of result.checks) {
684
1242
  const checkIcon = check.skipped ? CHECK_ICON.skip : check.passed ? CHECK_ICON.pass : CHECK_ICON.fail;
685
1243
  console.log(` ${checkIcon} ${check.name}: ${check.message}`);
686
1244
  if (showFix && !check.passed && !check.skipped && check.fix) {
687
- console.log(` ${pc2.yellow("\u2192")} Fix: ${pc2.cyan(check.fix)}`);
1245
+ console.log(` ${pc3.yellow("\u2192")} Fix: ${pc3.cyan(check.fix)}`);
688
1246
  }
689
1247
  }
690
1248
  console.log();
@@ -693,11 +1251,11 @@ async function runParallel(tasks, concurrency) {
693
1251
  const results = [];
694
1252
  const executing = /* @__PURE__ */ new Set();
695
1253
  for (const task of tasks) {
696
- const p8 = task().then((r) => {
1254
+ const p11 = task().then((r) => {
697
1255
  results.push(r);
698
- executing.delete(p8);
1256
+ executing.delete(p11);
699
1257
  });
700
- executing.add(p8);
1258
+ executing.add(p11);
701
1259
  if (executing.size >= concurrency) {
702
1260
  await Promise.race(executing);
703
1261
  }
@@ -706,121 +1264,173 @@ async function runParallel(tasks, concurrency) {
706
1264
  return results;
707
1265
  }
708
1266
 
709
- // src/commands/init.ts
710
- import { defineCommand as defineCommand3 } from "citty";
711
- import * as p from "@clack/prompts";
712
- import path3 from "path";
1267
+ // src/commands/info.ts
1268
+ import { defineCommand as defineCommand4 } from "citty";
1269
+ import pc4 from "picocolors";
1270
+ import { createSpinner as createSpinner2 } from "nanospinner";
713
1271
 
714
- // src/core/registry.ts
715
- import { createHash } from "crypto";
716
- function computeIntegrity(resolvedUrl) {
717
- const hash = createHash("sha512").update(resolvedUrl).digest("base64");
718
- return `sha512-${hash}`;
719
- }
720
- async function resolveFromSmithery(name) {
721
- const url = `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`;
722
- let data;
723
- try {
724
- const res = await fetch(url, {
725
- headers: { Accept: "application/json" },
726
- signal: AbortSignal.timeout(8e3)
727
- });
728
- if (res.status === 404) {
729
- throw new Error(`Server '${name}' not found on Smithery registry`);
730
- }
731
- if (!res.ok) {
732
- throw new Error(`Smithery API error: ${res.status}`);
1272
+ // src/core/package-info.ts
1273
+ async function buildInfo(name, entry, source = "npm") {
1274
+ const resolvedSource = entry?.source ?? source;
1275
+ let weeklyDownloads = 0;
1276
+ let maintainerCount = 0;
1277
+ let packageAge = 0;
1278
+ let lastPublish = "";
1279
+ let deprecated = false;
1280
+ let trustScore = null;
1281
+ let riskLevel = "UNKNOWN";
1282
+ if (resolvedSource === "npm") {
1283
+ const metadata = await fetchNpmMetadata(name);
1284
+ if (!metadata && !entry) {
1285
+ return null;
1286
+ }
1287
+ if (metadata) {
1288
+ weeklyDownloads = metadata.weeklyDownloads;
1289
+ maintainerCount = metadata.maintainerCount;
1290
+ packageAge = metadata.packageAge;
1291
+ lastPublish = metadata.lastPublish;
1292
+ deprecated = metadata.deprecated;
1293
+ const scored = computeTrustScore(metadata, []);
1294
+ trustScore = scored.score;
1295
+ riskLevel = scored.riskLevel;
733
1296
  }
734
- data = await res.json();
735
- } catch (err) {
736
- if (err instanceof Error && err.message.includes("not found")) throw err;
737
- throw new Error(
738
- `Cannot reach Smithery registry: ${err instanceof Error ? err.message : String(err)}`
739
- );
740
1297
  }
741
- const version = typeof data.version === "string" ? data.version : "latest";
742
- const command = typeof data.command === "string" ? data.command : "npx";
743
- const args = Array.isArray(data.args) ? data.args : ["-y", `${name}@${version}`];
744
- const envVars = Array.isArray(data.envVars) ? data.envVars : [];
745
- const resolved = typeof data.resolved === "string" ? data.resolved : `smithery:${name}@${version}`;
746
1298
  return {
747
1299
  name,
748
- version,
749
- description: typeof data.description === "string" ? data.description : "",
750
- runtime: "node",
751
- command,
752
- args,
753
- envVars,
754
- resolved
1300
+ version: entry?.version ?? "unknown",
1301
+ description: "",
1302
+ source: resolvedSource,
1303
+ runtime: entry?.runtime ?? "node",
1304
+ envVars: entry?.envVars ?? [],
1305
+ weeklyDownloads,
1306
+ maintainerCount,
1307
+ packageAge,
1308
+ lastPublish,
1309
+ deprecated,
1310
+ trustScore,
1311
+ riskLevel,
1312
+ installedClients: entry?.clients ?? [],
1313
+ isInstalled: entry !== null
755
1314
  };
756
1315
  }
757
- async function resolveFromNpm(packageName) {
758
- const url = `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`;
759
- let data;
760
- try {
761
- const res = await fetch(url, {
762
- headers: { Accept: "application/json" },
763
- signal: AbortSignal.timeout(8e3)
764
- });
765
- if (res.status === 404) {
766
- throw new Error(`Package '${packageName}' not found on npm`);
767
- }
768
- if (!res.ok) {
769
- throw new Error(`npm registry error: ${res.status}`);
770
- }
771
- data = await res.json();
772
- } catch (err) {
773
- if (err instanceof Error && err.message.includes("not found")) throw err;
774
- throw new Error(
775
- `Cannot reach npm registry: ${err instanceof Error ? err.message : String(err)}`
1316
+ async function getPackageInfo(serverName) {
1317
+ const lockfile = readLockfile();
1318
+ const entry = lockfile.servers[serverName] ?? null;
1319
+ return buildInfo(serverName, entry);
1320
+ }
1321
+
1322
+ // src/commands/info.ts
1323
+ function colorRisk2(score, riskLevel) {
1324
+ const label = score !== null ? `${score}/100 (${riskLevel})` : riskLevel;
1325
+ if (riskLevel === "LOW") return pc4.green(label);
1326
+ if (riskLevel === "MEDIUM") return pc4.yellow(label);
1327
+ if (riskLevel === "HIGH") return pc4.red(label);
1328
+ if (riskLevel === "CRITICAL") return pc4.bold(pc4.red(label));
1329
+ return pc4.dim(label);
1330
+ }
1331
+ function formatDaysAgo(isoDate) {
1332
+ if (!isoDate) return "unknown";
1333
+ const days = Math.floor((Date.now() - new Date(isoDate).getTime()) / 864e5);
1334
+ if (days === 0) return "today";
1335
+ if (days === 1) return "1 day ago";
1336
+ return `${days} days ago`;
1337
+ }
1338
+ function printInfo(info2) {
1339
+ const installedBadge = info2.isInstalled ? pc4.green(" [installed]") : pc4.dim(" [not installed]");
1340
+ console.log();
1341
+ console.log(pc4.bold(` ${info2.name}@${info2.version}`) + installedBadge);
1342
+ console.log(pc4.dim(" " + "\u2500".repeat(60)));
1343
+ console.log(` ${pc4.dim("Source:")} ${info2.source}`);
1344
+ console.log(` ${pc4.dim("Runtime:")} ${info2.runtime}`);
1345
+ if (info2.description) {
1346
+ console.log(` ${pc4.dim("Description:")} ${info2.description}`);
1347
+ }
1348
+ if (info2.deprecated) {
1349
+ console.log(` ${pc4.red("[DEPRECATED]")} This package is deprecated`);
1350
+ }
1351
+ console.log();
1352
+ console.log(` ${pc4.bold("Trust & Security")}`);
1353
+ console.log(` ${pc4.dim("Trust score:")} ${colorRisk2(info2.trustScore, info2.riskLevel)}`);
1354
+ if (info2.source === "npm") {
1355
+ console.log(
1356
+ ` ${pc4.dim("Downloads:")} ${info2.weeklyDownloads.toLocaleString()}/week ${pc4.dim("|")} ${pc4.dim("Age:")} ${info2.packageAge}d ${pc4.dim("|")} ${pc4.dim("Maintainers:")} ${info2.maintainerCount}`
776
1357
  );
1358
+ if (info2.lastPublish) {
1359
+ console.log(` ${pc4.dim("Last publish:")} ${formatDaysAgo(info2.lastPublish)}`);
1360
+ }
1361
+ } else {
1362
+ console.log(pc4.dim(" (Trust data available for npm packages only)"));
777
1363
  }
778
- const version = typeof data.version === "string" ? data.version : "latest";
779
- const resolved = `https://registry.npmjs.org/${packageName}/-/${packageName.replace(/^@[^/]+\//, "")}-${version}.tgz`;
780
- const mcpField = data.mcp && typeof data.mcp === "object" ? data.mcp : null;
781
- const envVars = mcpField?.envVars ? mcpField.envVars : [];
782
- return {
783
- name: packageName,
784
- version,
785
- description: typeof data.description === "string" ? data.description : "",
786
- runtime: "node",
787
- command: "npx",
788
- args: ["-y", `${packageName}@${version}`],
789
- envVars,
790
- resolved
791
- };
792
- }
793
- async function resolveFromGitHub(githubUrl) {
794
- const match = githubUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
795
- if (!match) {
796
- throw new Error(`Invalid GitHub URL: ${githubUrl}`);
1364
+ console.log();
1365
+ console.log(` ${pc4.bold("Environment Variables")}`);
1366
+ if (info2.envVars.length > 0) {
1367
+ for (const env of info2.envVars) {
1368
+ console.log(` ${pc4.cyan("\u2022")} ${env}`);
1369
+ }
1370
+ } else {
1371
+ console.log(pc4.dim(" none required"));
797
1372
  }
798
- const [, owner, repo] = match;
799
- const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/main/package.json`;
800
- let pkgData = {};
801
- try {
802
- const res = await fetch(rawUrl, { signal: AbortSignal.timeout(8e3) });
803
- if (res.ok) {
804
- pkgData = await res.json();
1373
+ console.log();
1374
+ console.log(` ${pc4.bold("Installed Clients")}`);
1375
+ if (info2.installedClients.length > 0) {
1376
+ for (const client of info2.installedClients) {
1377
+ console.log(` ${pc4.green("\u2713")} ${client}`);
805
1378
  }
806
- } catch {
1379
+ } else {
1380
+ console.log(pc4.dim(" Not installed in any client"));
807
1381
  }
808
- const version = typeof pkgData.version === "string" ? pkgData.version : "main";
809
- const name = typeof pkgData.name === "string" ? pkgData.name : `${owner}/${repo}`;
810
- return {
811
- name,
812
- version,
813
- description: typeof pkgData.description === "string" ? pkgData.description : "",
814
- runtime: "node",
815
- command: "npx",
816
- args: ["-y", githubUrl],
817
- envVars: [],
818
- resolved: githubUrl
819
- };
1382
+ console.log();
1383
+ console.log(pc4.dim(" " + "\u2500".repeat(60)));
1384
+ console.log();
820
1385
  }
1386
+ var info_default = defineCommand4({
1387
+ meta: {
1388
+ name: "info",
1389
+ description: "Show detailed metadata for an MCP server (installed or from registry)"
1390
+ },
1391
+ args: {
1392
+ server: {
1393
+ type: "positional",
1394
+ description: "Server name (e.g. @modelcontextprotocol/server-filesystem)",
1395
+ required: true
1396
+ },
1397
+ json: {
1398
+ type: "boolean",
1399
+ description: "Output results as JSON",
1400
+ default: false
1401
+ }
1402
+ },
1403
+ async run({ args }) {
1404
+ const spinner5 = createSpinner2(`Fetching info for ${args.server}...`).start();
1405
+ let info2;
1406
+ try {
1407
+ info2 = await getPackageInfo(args.server);
1408
+ } catch (err) {
1409
+ spinner5.error({ text: "Failed to fetch package info" });
1410
+ console.error(pc4.red(String(err)));
1411
+ process.exit(1);
1412
+ }
1413
+ if (!info2) {
1414
+ spinner5.error({ text: `Package not found: ${args.server}` });
1415
+ console.log(pc4.dim(`
1416
+ "${args.server}" was not found in the npm registry or your lockfile.
1417
+ `));
1418
+ process.exit(1);
1419
+ }
1420
+ spinner5.success({ text: `Found ${args.server}` });
1421
+ if (args.json) {
1422
+ console.log(JSON.stringify(info2, null, 2));
1423
+ return;
1424
+ }
1425
+ printInfo(info2);
1426
+ }
1427
+ });
821
1428
 
822
1429
  // src/commands/init.ts
823
- var init_default = defineCommand3({
1430
+ import { defineCommand as defineCommand5 } from "citty";
1431
+ import * as p3 from "@clack/prompts";
1432
+ import path4 from "path";
1433
+ var init_default = defineCommand5({
824
1434
  meta: {
825
1435
  name: "init",
826
1436
  description: "Initialize mcpman.lock in the current project"
@@ -835,27 +1445,27 @@ var init_default = defineCommand3({
835
1445
  },
836
1446
  async run({ args }) {
837
1447
  const nonInteractive = args.yes || !process.stdout.isTTY;
838
- p.intro("mcpman init");
839
- const targetPath = path3.join(process.cwd(), LOCKFILE_NAME);
1448
+ p3.intro("mcpman init");
1449
+ const targetPath = path4.join(process.cwd(), LOCKFILE_NAME);
840
1450
  const existing = findLockfile();
841
1451
  if (existing) {
842
1452
  if (nonInteractive) {
843
- p.log.warn(`Lockfile already exists: ${existing} \u2014 overwriting (non-interactive).`);
1453
+ p3.log.warn(`Lockfile already exists: ${existing} \u2014 overwriting (non-interactive).`);
844
1454
  } else {
845
- p.log.warn(`Lockfile already exists: ${existing}`);
846
- const overwrite = await p.confirm({ message: "Overwrite?" });
847
- if (p.isCancel(overwrite) || !overwrite) {
848
- p.outro("Cancelled.");
1455
+ p3.log.warn(`Lockfile already exists: ${existing}`);
1456
+ const overwrite = await p3.confirm({ message: "Overwrite?" });
1457
+ if (p3.isCancel(overwrite) || !overwrite) {
1458
+ p3.outro("Cancelled.");
849
1459
  return;
850
1460
  }
851
1461
  }
852
1462
  }
853
1463
  let clients = [];
854
1464
  try {
855
- const mod = await import("./client-detector-SUIJSIYM.js");
1465
+ const mod = await import("./client-detector-UAP2EYZA.js");
856
1466
  clients = await mod.getInstalledClients();
857
1467
  } catch {
858
- p.log.warn("Could not detect AI clients \u2014 creating empty lockfile.");
1468
+ p3.log.warn("Could not detect AI clients \u2014 creating empty lockfile.");
859
1469
  }
860
1470
  const clientServers = [];
861
1471
  for (const client of clients) {
@@ -869,26 +1479,26 @@ var init_default = defineCommand3({
869
1479
  }
870
1480
  createEmptyLockfile(targetPath);
871
1481
  if (clientServers.length === 0) {
872
- p.log.info("No existing servers found in any client config.");
873
- p.outro(`Created ${LOCKFILE_NAME} \u2014 add it to version control!`);
1482
+ p3.log.info("No existing servers found in any client config.");
1483
+ p3.outro(`Created ${LOCKFILE_NAME} \u2014 add it to version control!`);
874
1484
  return;
875
1485
  }
876
1486
  let selected;
877
1487
  if (nonInteractive) {
878
1488
  selected = clientServers.map((cs) => cs.client.type);
879
- p.log.info(`Non-interactive mode: importing all ${clientServers.length} client(s).`);
1489
+ p3.log.info(`Non-interactive mode: importing all ${clientServers.length} client(s).`);
880
1490
  } else {
881
1491
  const options = clientServers.map((cs) => ({
882
1492
  value: cs.client.type,
883
1493
  label: `${cs.client.displayName} (${Object.keys(cs.servers).length} servers)`
884
1494
  }));
885
- const toImport = await p.multiselect({
1495
+ const toImport = await p3.multiselect({
886
1496
  message: "Import existing servers into lockfile?",
887
1497
  options,
888
1498
  required: false
889
1499
  });
890
- if (p.isCancel(toImport)) {
891
- p.outro(`Created empty ${LOCKFILE_NAME}`);
1500
+ if (p3.isCancel(toImport)) {
1501
+ p3.outro(`Created empty ${LOCKFILE_NAME}`);
892
1502
  return;
893
1503
  }
894
1504
  selected = toImport;
@@ -914,121 +1524,122 @@ var init_default = defineCommand3({
914
1524
  importCount++;
915
1525
  }
916
1526
  }
917
- p.outro(
1527
+ p3.outro(
918
1528
  `Created ${LOCKFILE_NAME} with ${importCount} server(s) \u2014 commit to version control!`
919
1529
  );
920
1530
  }
921
1531
  });
922
1532
 
923
1533
  // src/commands/install.ts
924
- import { defineCommand as defineCommand4 } from "citty";
1534
+ import { defineCommand as defineCommand6 } from "citty";
925
1535
 
926
1536
  // src/core/installer.ts
927
- import * as p2 from "@clack/prompts";
1537
+ import * as p5 from "@clack/prompts";
928
1538
 
929
- // src/core/server-resolver.ts
930
- function detectSource(input) {
931
- if (input.startsWith("smithery:")) {
932
- return { type: "smithery", input: input.slice(9) };
933
- }
934
- if (input.startsWith("https://github.com/") || input.startsWith("github.com/")) {
935
- return { type: "github", input };
936
- }
937
- return { type: "npm", input };
938
- }
939
- function parseEnvFlags(envFlags) {
940
- if (!envFlags) return {};
941
- const flags = Array.isArray(envFlags) ? envFlags : [envFlags];
942
- const result = {};
943
- for (const flag of flags) {
944
- const idx = flag.indexOf("=");
945
- if (idx > 0) {
946
- result[flag.slice(0, idx)] = flag.slice(idx + 1);
1539
+ // src/core/installer-vault-helpers.ts
1540
+ import * as p4 from "@clack/prompts";
1541
+ async function tryLoadVaultSecrets(serverName) {
1542
+ try {
1543
+ const entries = listSecrets(serverName);
1544
+ if (entries.length === 0 || entries[0].keys.length === 0) {
1545
+ return {};
947
1546
  }
1547
+ const password = await getMasterPassword();
1548
+ return getSecretsForServer(serverName, password);
1549
+ } catch {
1550
+ return {};
948
1551
  }
949
- return result;
950
1552
  }
951
- async function resolveServer(input) {
952
- const source = detectSource(input);
953
- switch (source.type) {
954
- case "smithery":
955
- return resolveFromSmithery(source.input);
956
- case "github":
957
- return resolveFromGitHub(source.input);
958
- case "npm":
959
- return resolveFromNpm(source.input);
1553
+ async function offerVaultSave(serverName, newVars, yes) {
1554
+ if (Object.keys(newVars).length === 0) return;
1555
+ if (yes) return;
1556
+ try {
1557
+ const save = await p4.confirm({
1558
+ message: `Save ${Object.keys(newVars).length} env var(s) to encrypted vault for future installs?`
1559
+ });
1560
+ if (p4.isCancel(save) || !save) return;
1561
+ const password = await getMasterPassword();
1562
+ for (const [key, value] of Object.entries(newVars)) {
1563
+ setSecret(serverName, key, value, password);
1564
+ }
1565
+ p4.log.success(`Credentials saved to vault for '${serverName}'`);
1566
+ } catch (err) {
1567
+ p4.log.warn(`Could not save to vault: ${err instanceof Error ? err.message : String(err)}`);
960
1568
  }
961
1569
  }
962
1570
 
963
1571
  // src/core/installer.ts
964
- async function loadClients() {
1572
+ async function loadClients2() {
965
1573
  try {
966
- const mod = await import("./client-detector-SUIJSIYM.js");
1574
+ const mod = await import("./client-detector-UAP2EYZA.js");
967
1575
  return mod.getInstalledClients();
968
1576
  } catch {
969
1577
  return [];
970
1578
  }
971
1579
  }
972
1580
  async function installServer(input, options = {}) {
973
- p2.intro("mcpman install");
974
- const spinner5 = p2.spinner();
1581
+ p5.intro("mcpman install");
1582
+ const spinner5 = p5.spinner();
975
1583
  spinner5.start("Resolving server...");
976
1584
  let metadata;
977
1585
  try {
978
1586
  metadata = await resolveServer(input);
979
1587
  } catch (err) {
980
1588
  spinner5.stop("Resolution failed");
981
- p2.log.error(err instanceof Error ? err.message : String(err));
1589
+ p5.log.error(err instanceof Error ? err.message : String(err));
982
1590
  process.exit(1);
983
1591
  }
984
1592
  spinner5.stop(`Found: ${metadata.name}@${metadata.version}`);
985
- const clients = await loadClients();
1593
+ const clients = await loadClients2();
986
1594
  if (clients.length === 0) {
987
- p2.log.warn("No supported AI clients detected on this machine.");
988
- p2.log.info("Supported: Claude Desktop, Cursor, VS Code, Windsurf");
1595
+ p5.log.warn("No supported AI clients detected on this machine.");
1596
+ p5.log.info("Supported: Claude Desktop, Cursor, VS Code, Windsurf");
989
1597
  process.exit(1);
990
1598
  }
991
1599
  let selectedClients;
992
1600
  if (options.client) {
993
1601
  const found = clients.find((c) => c.type === options.client || c.displayName.toLowerCase() === options.client?.toLowerCase());
994
1602
  if (!found) {
995
- p2.log.error(`Client '${options.client}' not found or not installed.`);
996
- p2.log.info(`Available: ${clients.map((c) => c.type).join(", ")}`);
1603
+ p5.log.error(`Client '${options.client}' not found or not installed.`);
1604
+ p5.log.info(`Available: ${clients.map((c) => c.type).join(", ")}`);
997
1605
  process.exit(1);
998
1606
  }
999
1607
  selectedClients = [found];
1000
1608
  } else if (options.yes || clients.length === 1) {
1001
1609
  selectedClients = clients;
1002
1610
  } else {
1003
- const chosen = await p2.multiselect({
1611
+ const chosen = await p5.multiselect({
1004
1612
  message: "Install to which client(s)?",
1005
1613
  options: clients.map((c) => ({ value: c.type, label: c.displayName })),
1006
1614
  required: true
1007
1615
  });
1008
- if (p2.isCancel(chosen)) {
1009
- p2.outro("Cancelled.");
1616
+ if (p5.isCancel(chosen)) {
1617
+ p5.outro("Cancelled.");
1010
1618
  process.exit(0);
1011
1619
  }
1012
1620
  selectedClients = clients.filter((c) => chosen.includes(c.type));
1013
1621
  }
1014
1622
  const providedEnv = parseEnvFlags(options.env);
1015
- const collectedEnv = { ...providedEnv };
1623
+ const vaultEnv = await tryLoadVaultSecrets(metadata.name);
1624
+ const collectedEnv = { ...vaultEnv, ...providedEnv };
1625
+ const newlyEnteredVars = {};
1016
1626
  const requiredVars = metadata.envVars.filter((e) => e.required && !(e.name in collectedEnv));
1017
1627
  for (const envVar of requiredVars) {
1018
1628
  if (options.yes && envVar.default) {
1019
1629
  collectedEnv[envVar.name] = envVar.default;
1020
1630
  continue;
1021
1631
  }
1022
- const val = await p2.text({
1632
+ const val = await p5.text({
1023
1633
  message: `${envVar.name}${envVar.description ? ` \u2014 ${envVar.description}` : ""}`,
1024
1634
  placeholder: envVar.default ?? "",
1025
1635
  validate: (v) => envVar.required && !v ? "Required" : void 0
1026
1636
  });
1027
- if (p2.isCancel(val)) {
1028
- p2.outro("Cancelled.");
1637
+ if (p5.isCancel(val)) {
1638
+ p5.outro("Cancelled.");
1029
1639
  process.exit(0);
1030
1640
  }
1031
1641
  collectedEnv[envVar.name] = val;
1642
+ newlyEnteredVars[envVar.name] = val;
1032
1643
  }
1033
1644
  const entry = {
1034
1645
  command: metadata.command,
@@ -1043,7 +1654,7 @@ async function installServer(input, options = {}) {
1043
1654
  clientTypes.push(client.type);
1044
1655
  } catch (err) {
1045
1656
  spinner5.stop("Partial failure");
1046
- p2.log.warn(`Failed to write to ${client.displayName}: ${err instanceof Error ? err.message : String(err)}`);
1657
+ p5.log.warn(`Failed to write to ${client.displayName}: ${err instanceof Error ? err.message : String(err)}`);
1047
1658
  }
1048
1659
  }
1049
1660
  spinner5.stop("Config written");
@@ -1062,12 +1673,13 @@ async function installServer(input, options = {}) {
1062
1673
  clients: clientTypes
1063
1674
  });
1064
1675
  const lockPath = findLockfile() ?? "mcpman.lock (global)";
1065
- p2.log.success(`Lockfile updated: ${lockPath}`);
1066
- p2.outro(`${metadata.name}@${metadata.version} installed to ${clientTypes.join(", ")}`);
1676
+ p5.log.success(`Lockfile updated: ${lockPath}`);
1677
+ await offerVaultSave(metadata.name, newlyEnteredVars, options.yes ?? false);
1678
+ p5.outro(`${metadata.name}@${metadata.version} installed to ${clientTypes.join(", ")}`);
1067
1679
  }
1068
1680
 
1069
1681
  // src/utils/logger.ts
1070
- import pc3 from "picocolors";
1682
+ import pc5 from "picocolors";
1071
1683
  var noColor = process.env.NO_COLOR !== void 0 || process.argv.includes("--no-color");
1072
1684
  var isVerbose = process.argv.includes("--verbose");
1073
1685
  var isJson = process.argv.includes("--json");
@@ -1076,19 +1688,19 @@ function colorize(fn, text2) {
1076
1688
  }
1077
1689
  function info(message) {
1078
1690
  if (isJson) return;
1079
- console.log(`${colorize(pc3.cyan, "i")} ${message}`);
1691
+ console.log(`${colorize(pc5.cyan, "i")} ${message}`);
1080
1692
  }
1081
1693
  function error(message) {
1082
1694
  if (isJson) return;
1083
- console.error(`${colorize(pc3.red, "\u2717")} ${message}`);
1695
+ console.error(`${colorize(pc5.red, "\u2717")} ${message}`);
1084
1696
  }
1085
1697
  function json(data) {
1086
1698
  console.log(JSON.stringify(data, null, 2));
1087
1699
  }
1088
1700
 
1089
1701
  // src/commands/install.ts
1090
- import * as p3 from "@clack/prompts";
1091
- var install_default = defineCommand4({
1702
+ import * as p6 from "@clack/prompts";
1703
+ var install_default = defineCommand6({
1092
1704
  meta: {
1093
1705
  name: "install",
1094
1706
  description: "Install an MCP server into one or more AI clients"
@@ -1137,8 +1749,8 @@ async function restoreFromLockfile() {
1137
1749
  info("Lockfile is empty \u2014 nothing to restore.");
1138
1750
  return;
1139
1751
  }
1140
- p3.intro(`mcpman install (restore from ${lockPath})`);
1141
- p3.log.info(`Restoring ${entries.length} server(s)...`);
1752
+ p6.intro(`mcpman install (restore from ${lockPath})`);
1753
+ p6.log.info(`Restoring ${entries.length} server(s)...`);
1142
1754
  for (const [name, entry] of entries) {
1143
1755
  const input = entry.source === "smithery" ? `smithery:${name}` : entry.source === "github" ? entry.resolved : name;
1144
1756
  await installServer(input, {
@@ -1146,18 +1758,18 @@ async function restoreFromLockfile() {
1146
1758
  yes: true
1147
1759
  });
1148
1760
  }
1149
- p3.outro("Restore complete.");
1761
+ p6.outro("Restore complete.");
1150
1762
  }
1151
1763
 
1152
1764
  // src/commands/list.ts
1153
- import { defineCommand as defineCommand5 } from "citty";
1154
- import pc4 from "picocolors";
1765
+ import { defineCommand as defineCommand7 } from "citty";
1766
+ import pc6 from "picocolors";
1155
1767
  var STATUS_ICON = {
1156
- healthy: pc4.green("\u25CF"),
1157
- unhealthy: pc4.red("\u25CF"),
1158
- unknown: pc4.dim("\u25CB")
1768
+ healthy: pc6.green("\u25CF"),
1769
+ unhealthy: pc6.red("\u25CF"),
1770
+ unknown: pc6.dim("\u25CB")
1159
1771
  };
1160
- var list_default = defineCommand5({
1772
+ var list_default = defineCommand7({
1161
1773
  meta: {
1162
1774
  name: "list",
1163
1775
  description: "List installed MCP servers"
@@ -1177,7 +1789,7 @@ var list_default = defineCommand5({
1177
1789
  const servers = await getInstalledServers(args.client);
1178
1790
  if (servers.length === 0) {
1179
1791
  const filter = args.client ? ` for client "${args.client}"` : "";
1180
- console.log(pc4.dim(`No MCP servers installed${filter}. Run ${pc4.cyan("mcpman install <server>")} to get started.`));
1792
+ console.log(pc6.dim(`No MCP servers installed${filter}. Run ${pc6.cyan("mcpman install <server>")} to get started.`));
1181
1793
  return;
1182
1794
  }
1183
1795
  const withStatus = await Promise.all(
@@ -1201,8 +1813,8 @@ var list_default = defineCommand5({
1201
1813
  const nameWidth = Math.max(4, ...withStatus.map((s) => s.name.length));
1202
1814
  const clientsWidth = Math.max(7, ...withStatus.map((s) => formatClients(s.clients).length));
1203
1815
  const header = ` ${pad("NAME", nameWidth)} ${pad("CLIENT(S)", clientsWidth)} ${pad("COMMAND", 20)} STATUS`;
1204
- console.log(pc4.dim(header));
1205
- console.log(pc4.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientsWidth)} ${"-".repeat(20)} ------`));
1816
+ console.log(pc6.dim(header));
1817
+ console.log(pc6.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientsWidth)} ${"-".repeat(20)} ------`));
1206
1818
  for (const s of withStatus) {
1207
1819
  const icon = STATUS_ICON[s.status];
1208
1820
  const clientsStr = formatClients(s.clients);
@@ -1210,7 +1822,7 @@ var list_default = defineCommand5({
1210
1822
  console.log(` ${pad(s.name, nameWidth)} ${pad(clientsStr, clientsWidth)} ${pad(cmdStr, 20)} ${icon} ${s.status}`);
1211
1823
  }
1212
1824
  const clientSet = new Set(withStatus.flatMap((s) => s.clients));
1213
- console.log(pc4.dim(`
1825
+ console.log(pc6.dim(`
1214
1826
  ${withStatus.length} server${withStatus.length !== 1 ? "s" : ""} \xB7 ${clientSet.size} client${clientSet.size !== 1 ? "s" : ""}`));
1215
1827
  }
1216
1828
  });
@@ -1231,9 +1843,9 @@ function formatClients(clients) {
1231
1843
  }
1232
1844
 
1233
1845
  // src/commands/remove.ts
1234
- import { defineCommand as defineCommand6 } from "citty";
1235
- import * as p4 from "@clack/prompts";
1236
- import pc5 from "picocolors";
1846
+ import { defineCommand as defineCommand8 } from "citty";
1847
+ import * as p7 from "@clack/prompts";
1848
+ import pc7 from "picocolors";
1237
1849
  var CLIENT_DISPLAY2 = {
1238
1850
  "claude-desktop": "Claude",
1239
1851
  cursor: "Cursor",
@@ -1243,7 +1855,7 @@ var CLIENT_DISPLAY2 = {
1243
1855
  function clientDisplayName(type) {
1244
1856
  return CLIENT_DISPLAY2[type] ?? type;
1245
1857
  }
1246
- var remove_default = defineCommand6({
1858
+ var remove_default = defineCommand8({
1247
1859
  meta: {
1248
1860
  name: "remove",
1249
1861
  description: "Remove an MCP server from one or more AI clients"
@@ -1270,17 +1882,17 @@ var remove_default = defineCommand6({
1270
1882
  }
1271
1883
  },
1272
1884
  async run({ args }) {
1273
- p4.intro(pc5.bold("mcpman remove"));
1885
+ p7.intro(pc7.bold("mcpman remove"));
1274
1886
  const serverName = args.server;
1275
1887
  const servers = await getInstalledServers();
1276
1888
  const match = servers.find((s) => s.name === serverName);
1277
1889
  if (!match) {
1278
- p4.log.warn(`Server "${serverName}" is not installed.`);
1890
+ p7.log.warn(`Server "${serverName}" is not installed.`);
1279
1891
  const similar = servers.filter((s) => s.name.includes(serverName) || serverName.includes(s.name));
1280
1892
  if (similar.length > 0) {
1281
- p4.log.info(`Did you mean: ${similar.map((s) => pc5.cyan(s.name)).join(", ")}?`);
1893
+ p7.log.info(`Did you mean: ${similar.map((s) => pc7.cyan(s.name)).join(", ")}?`);
1282
1894
  }
1283
- p4.outro("Nothing to remove.");
1895
+ p7.outro("Nothing to remove.");
1284
1896
  return;
1285
1897
  }
1286
1898
  let targetClients;
@@ -1288,15 +1900,15 @@ var remove_default = defineCommand6({
1288
1900
  targetClients = match.clients;
1289
1901
  } else if (args.client) {
1290
1902
  if (!match.clients.includes(args.client)) {
1291
- p4.log.warn(`Server "${serverName}" is not installed in client "${args.client}".`);
1292
- p4.outro("Nothing to remove.");
1903
+ p7.log.warn(`Server "${serverName}" is not installed in client "${args.client}".`);
1904
+ p7.outro("Nothing to remove.");
1293
1905
  return;
1294
1906
  }
1295
1907
  targetClients = [args.client];
1296
1908
  } else if (match.clients.length === 1) {
1297
1909
  targetClients = match.clients;
1298
1910
  } else {
1299
- const selected = await p4.multiselect({
1911
+ const selected = await p7.multiselect({
1300
1912
  message: `Remove "${serverName}" from which clients?`,
1301
1913
  options: match.clients.map((c) => ({
1302
1914
  value: c,
@@ -1304,19 +1916,19 @@ var remove_default = defineCommand6({
1304
1916
  })),
1305
1917
  required: true
1306
1918
  });
1307
- if (p4.isCancel(selected)) {
1308
- p4.outro("Cancelled.");
1919
+ if (p7.isCancel(selected)) {
1920
+ p7.outro("Cancelled.");
1309
1921
  process.exit(0);
1310
1922
  }
1311
1923
  targetClients = selected;
1312
1924
  }
1313
1925
  if (!args.yes) {
1314
1926
  const clientNames = targetClients.map(clientDisplayName).join(", ");
1315
- const confirmed = await p4.confirm({
1316
- message: `Remove ${pc5.cyan(serverName)} from ${pc5.yellow(clientNames)}?`
1927
+ const confirmed = await p7.confirm({
1928
+ message: `Remove ${pc7.cyan(serverName)} from ${pc7.yellow(clientNames)}?`
1317
1929
  });
1318
- if (p4.isCancel(confirmed) || !confirmed) {
1319
- p4.outro("Cancelled.");
1930
+ if (p7.isCancel(confirmed) || !confirmed) {
1931
+ p7.outro("Cancelled.");
1320
1932
  return;
1321
1933
  }
1322
1934
  }
@@ -1330,25 +1942,268 @@ var remove_default = defineCommand6({
1330
1942
  }
1331
1943
  try {
1332
1944
  await handler.removeServer(serverName);
1333
- p4.log.success(`Removed from ${clientDisplayName(clientType)}`);
1945
+ p7.log.success(`Removed from ${clientDisplayName(clientType)}`);
1334
1946
  } catch (err) {
1335
1947
  const msg = err instanceof Error ? err.message : String(err);
1336
1948
  errors.push(`${clientDisplayName(clientType)}: ${msg}`);
1337
1949
  }
1338
1950
  }
1339
1951
  if (errors.length > 0) {
1340
- for (const e of errors) p4.log.error(e);
1341
- p4.outro(pc5.red("Completed with errors."));
1952
+ for (const e of errors) p7.log.error(e);
1953
+ p7.outro(pc7.red("Completed with errors."));
1342
1954
  process.exit(1);
1343
1955
  }
1344
- p4.outro(pc5.green(`Removed "${serverName}" successfully.`));
1956
+ p7.outro(pc7.green(`Removed "${serverName}" successfully.`));
1957
+ }
1958
+ });
1959
+
1960
+ // src/commands/run.ts
1961
+ import { defineCommand as defineCommand9 } from "citty";
1962
+ import { spawn as spawn2 } from "child_process";
1963
+ import pc8 from "picocolors";
1964
+ var run_default = defineCommand9({
1965
+ meta: {
1966
+ name: "run",
1967
+ description: "Run an installed MCP server with vault secrets injected"
1968
+ },
1969
+ args: {
1970
+ server: {
1971
+ type: "positional",
1972
+ description: "Server name to run (as installed in lockfile)",
1973
+ required: true
1974
+ },
1975
+ env: {
1976
+ type: "string",
1977
+ description: "Override env var KEY=VAL (repeatable)",
1978
+ alias: "e"
1979
+ }
1980
+ },
1981
+ async run({ args }) {
1982
+ const serverName = args.server;
1983
+ const lockfile = readLockfile();
1984
+ const entry = lockfile.servers[serverName];
1985
+ if (!entry) {
1986
+ console.error(pc8.red(` Error: Server '${serverName}' is not installed.`));
1987
+ console.error(pc8.dim(` Run ${pc8.cyan("mcpman install <server>")} to install it first.`));
1988
+ process.exit(1);
1989
+ }
1990
+ const lockfileEnv = parseEnvFlags(entry.envVars);
1991
+ const vaultEnv = await loadVaultSecrets(serverName);
1992
+ const cliEnv = parseEnvFlags(args.env);
1993
+ const finalEnv = {
1994
+ ...process.env,
1995
+ ...lockfileEnv,
1996
+ ...vaultEnv,
1997
+ ...cliEnv
1998
+ };
1999
+ console.log(pc8.dim(` Running ${pc8.cyan(serverName)}...`));
2000
+ const child = spawn2(entry.command, entry.args, {
2001
+ env: finalEnv,
2002
+ stdio: "inherit"
2003
+ });
2004
+ const forwardSignal = (signal) => {
2005
+ if (!child.killed) {
2006
+ child.kill(signal);
2007
+ }
2008
+ };
2009
+ process.on("SIGINT", () => forwardSignal("SIGINT"));
2010
+ process.on("SIGTERM", () => forwardSignal("SIGTERM"));
2011
+ await new Promise((resolve) => {
2012
+ child.on("close", (code) => {
2013
+ process.exit(code ?? 0);
2014
+ resolve();
2015
+ });
2016
+ child.on("error", (err) => {
2017
+ console.error(pc8.red(` Failed to start '${serverName}': ${err.message}`));
2018
+ process.exit(1);
2019
+ resolve();
2020
+ });
2021
+ });
2022
+ }
2023
+ });
2024
+ async function loadVaultSecrets(serverName) {
2025
+ try {
2026
+ const entries = listSecrets(serverName);
2027
+ if (entries.length === 0 || entries[0].keys.length === 0) {
2028
+ return {};
2029
+ }
2030
+ const password = await getMasterPassword();
2031
+ return getSecretsForServer(serverName, password);
2032
+ } catch {
2033
+ console.warn(pc8.yellow(" Warning: Could not load vault secrets, continuing without them."));
2034
+ return {};
2035
+ }
2036
+ }
2037
+
2038
+ // src/commands/search.ts
2039
+ import { defineCommand as defineCommand10 } from "citty";
2040
+ import pc9 from "picocolors";
2041
+ import { createSpinner as createSpinner3 } from "nanospinner";
2042
+
2043
+ // src/core/registry-search.ts
2044
+ var SEARCH_TIMEOUT_MS = 1e4;
2045
+ async function searchNpm(query, limit = 20) {
2046
+ const cap = Math.min(limit, 100);
2047
+ const url = `https://registry.npmjs.org/-/v1/search?text=mcp+${encodeURIComponent(query)}&size=${cap}`;
2048
+ try {
2049
+ const res = await fetch(url, { signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS) });
2050
+ if (!res.ok) return [];
2051
+ const data = await res.json();
2052
+ const objects = Array.isArray(data["objects"]) ? data["objects"] : [];
2053
+ return objects.map((obj) => {
2054
+ const pkg = obj["package"] ?? {};
2055
+ const dl = obj["downloads"];
2056
+ return {
2057
+ name: typeof pkg["name"] === "string" ? pkg["name"] : "",
2058
+ description: typeof pkg["description"] === "string" ? pkg["description"] : "",
2059
+ version: typeof pkg["version"] === "string" ? pkg["version"] : "",
2060
+ date: typeof pkg["date"] === "string" ? pkg["date"] : "",
2061
+ downloads: typeof dl?.["weekly"] === "number" ? dl["weekly"] : 0,
2062
+ keywords: Array.isArray(pkg["keywords"]) ? pkg["keywords"] : []
2063
+ };
2064
+ }).filter((r) => r.name !== "");
2065
+ } catch {
2066
+ return [];
2067
+ }
2068
+ }
2069
+ async function searchSmithery(query, limit = 20) {
2070
+ const cap = Math.min(limit, 100);
2071
+ const url = `https://api.smithery.ai/v1/servers?q=${encodeURIComponent(query)}&limit=${cap}`;
2072
+ try {
2073
+ const res = await fetch(url, { signal: AbortSignal.timeout(SEARCH_TIMEOUT_MS) });
2074
+ if (!res.ok) return [];
2075
+ const data = await res.json();
2076
+ const servers = Array.isArray(data["servers"]) ? data["servers"] : [];
2077
+ return servers.map((s) => ({
2078
+ name: typeof s["name"] === "string" ? s["name"] : "",
2079
+ description: typeof s["description"] === "string" ? s["description"] : "",
2080
+ version: typeof s["version"] === "string" ? s["version"] : "latest",
2081
+ runtime: typeof s["runtime"] === "string" ? s["runtime"] : "node"
2082
+ })).filter((r) => r.name !== "");
2083
+ } catch {
2084
+ return [];
2085
+ }
2086
+ }
2087
+
2088
+ // src/commands/search.ts
2089
+ function truncate2(s, max) {
2090
+ return s.length > max ? s.slice(0, max - 1) + "\u2026" : s;
2091
+ }
2092
+ function pad2(s, width) {
2093
+ return s.length >= width ? s : s + " ".repeat(width - s.length);
2094
+ }
2095
+ function highlightMatch(name, query) {
2096
+ const idx = name.toLowerCase().indexOf(query.toLowerCase());
2097
+ if (idx === -1) return name;
2098
+ return name.slice(0, idx) + pc9.yellow(name.slice(idx, idx + query.length)) + name.slice(idx + query.length);
2099
+ }
2100
+ function formatDownloads(n) {
2101
+ if (!n) return pc9.dim("\u2014");
2102
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
2103
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
2104
+ return String(n);
2105
+ }
2106
+ function printNpmResults(results, query) {
2107
+ const nameWidth = Math.max(4, ...results.map((r) => r.name.length), 20);
2108
+ const verWidth = Math.max(7, ...results.map((r) => r.version.length));
2109
+ const dlWidth = 9;
2110
+ const descMax = 50;
2111
+ const header = ` ${pad2("NAME", nameWidth)} ${pad2("VERSION", verWidth)} ${pad2("DOWNLOADS", dlWidth)} DESCRIPTION`;
2112
+ console.log(pc9.dim(header));
2113
+ console.log(pc9.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(verWidth)} ${"-".repeat(dlWidth)} ${"-".repeat(descMax)}`));
2114
+ for (const r of results) {
2115
+ const name = highlightMatch(pad2(r.name, nameWidth), query);
2116
+ const ver = pad2(r.version, verWidth);
2117
+ const dl = pad2(formatDownloads(r.downloads), dlWidth);
2118
+ const desc = truncate2(r.description || pc9.dim("(no description)"), descMax);
2119
+ console.log(` ${name} ${pc9.dim(ver)} ${dl} ${desc}`);
2120
+ }
2121
+ }
2122
+ function printSmitheryResults(results, query) {
2123
+ const nameWidth = Math.max(4, ...results.map((r) => r.name.length), 20);
2124
+ const verWidth = Math.max(7, ...results.map((r) => r.version.length));
2125
+ const descMax = 50;
2126
+ const header = ` ${pad2("NAME", nameWidth)} ${pad2("VERSION", verWidth)} DESCRIPTION`;
2127
+ console.log(pc9.dim(header));
2128
+ console.log(pc9.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(verWidth)} ${"-".repeat(descMax)}`));
2129
+ for (const r of results) {
2130
+ const name = highlightMatch(pad2(r.name, nameWidth), query);
2131
+ const ver = pad2(r.version, verWidth);
2132
+ const desc = truncate2(r.description || pc9.dim("(no description)"), descMax);
2133
+ console.log(` ${name} ${pc9.dim(ver)} ${desc}`);
2134
+ }
2135
+ }
2136
+ var search_default = defineCommand10({
2137
+ meta: {
2138
+ name: "search",
2139
+ description: "Search for MCP servers on npm or Smithery registry"
2140
+ },
2141
+ args: {
2142
+ query: {
2143
+ type: "positional",
2144
+ description: "Search query",
2145
+ required: true
2146
+ },
2147
+ registry: {
2148
+ type: "string",
2149
+ description: "Registry to search: npm or smithery (default: npm)",
2150
+ default: "npm"
2151
+ },
2152
+ limit: {
2153
+ type: "string",
2154
+ description: "Maximum number of results (default: 20, max: 100)",
2155
+ default: "20"
2156
+ }
2157
+ },
2158
+ async run({ args }) {
2159
+ const query = args.query;
2160
+ const registry = args.registry.toLowerCase();
2161
+ const limit = Math.min(Math.max(1, Number.parseInt(args.limit, 10) || 20), 100);
2162
+ if (registry !== "npm" && registry !== "smithery") {
2163
+ console.error(pc9.red(` Unknown registry "${registry}". Use "npm" or "smithery".`));
2164
+ process.exit(1);
2165
+ }
2166
+ const spinner5 = createSpinner3(`Searching ${registry} for "${query}"...`).start();
2167
+ if (registry === "npm") {
2168
+ const results2 = await searchNpm(query, limit);
2169
+ spinner5.stop();
2170
+ if (results2.length === 0) {
2171
+ console.log(pc9.dim(`
2172
+ No results found for "${query}" on npm.
2173
+ `));
2174
+ return;
2175
+ }
2176
+ console.log(pc9.bold(`
2177
+ mcpman search \u2014 npm (${results2.length} result${results2.length !== 1 ? "s" : ""})
2178
+ `));
2179
+ printNpmResults(results2, query);
2180
+ console.log(pc9.dim(`
2181
+ Install with: mcpman install <name>
2182
+ `));
2183
+ return;
2184
+ }
2185
+ const results = await searchSmithery(query, limit);
2186
+ spinner5.stop();
2187
+ if (results.length === 0) {
2188
+ console.log(pc9.dim(`
2189
+ No results found for "${query}" on Smithery.
2190
+ `));
2191
+ return;
2192
+ }
2193
+ console.log(pc9.bold(`
2194
+ mcpman search \u2014 Smithery (${results.length} result${results.length !== 1 ? "s" : ""})
2195
+ `));
2196
+ printSmitheryResults(results, query);
2197
+ console.log(pc9.dim(`
2198
+ Install with: mcpman install <name>
2199
+ `));
1345
2200
  }
1346
2201
  });
1347
2202
 
1348
2203
  // src/commands/secrets.ts
1349
- import { defineCommand as defineCommand7 } from "citty";
1350
- import pc6 from "picocolors";
1351
- import * as p5 from "@clack/prompts";
2204
+ import { defineCommand as defineCommand11 } from "citty";
2205
+ import pc10 from "picocolors";
2206
+ import * as p8 from "@clack/prompts";
1352
2207
  function maskValue(value) {
1353
2208
  if (value.length <= 8) return "***";
1354
2209
  return `${value.slice(0, 4)}***${value.slice(-3)}`;
@@ -1358,7 +2213,7 @@ function parseKeyValue(input) {
1358
2213
  if (idx <= 0) return null;
1359
2214
  return { key: input.slice(0, idx), value: input.slice(idx + 1) };
1360
2215
  }
1361
- var setCommand = defineCommand7({
2216
+ var setCommand2 = defineCommand11({
1362
2217
  meta: { name: "set", description: "Store an encrypted secret for a server" },
1363
2218
  args: {
1364
2219
  server: {
@@ -1375,30 +2230,30 @@ var setCommand = defineCommand7({
1375
2230
  async run({ args }) {
1376
2231
  const parsed = parseKeyValue(args.keyvalue);
1377
2232
  if (!parsed) {
1378
- console.error(pc6.red("\u2717") + " Invalid format. Expected KEY=VALUE");
2233
+ console.error(pc10.red("\u2717") + " Invalid format. Expected KEY=VALUE");
1379
2234
  process.exit(1);
1380
2235
  }
1381
- p5.intro(pc6.cyan("mcpman secrets set"));
2236
+ p8.intro(pc10.cyan("mcpman secrets set"));
1382
2237
  const isNew = listSecrets(args.server).length === 0 || !listSecrets(args.server)[0]?.keys.includes(parsed.key);
1383
2238
  const vaultPath = (await import("./vault-service-UTZAV6N6.js")).getVaultPath();
1384
2239
  const vaultExists = (await import("fs")).existsSync(vaultPath);
1385
2240
  const password = await getMasterPassword(!vaultExists && isNew);
1386
- const spin = p5.spinner();
2241
+ const spin = p8.spinner();
1387
2242
  spin.start("Encrypting secret...");
1388
2243
  try {
1389
2244
  setSecret(args.server, parsed.key, parsed.value, password);
1390
2245
  spin.stop(
1391
- `${pc6.green("\u2713")} Stored ${pc6.bold(parsed.key)} for ${pc6.cyan(args.server)}`
2246
+ `${pc10.green("\u2713")} Stored ${pc10.bold(parsed.key)} for ${pc10.cyan(args.server)}`
1392
2247
  );
1393
2248
  } catch (err) {
1394
- spin.stop(pc6.red("\u2717") + " Failed to store secret");
1395
- console.error(pc6.dim(String(err)));
2249
+ spin.stop(pc10.red("\u2717") + " Failed to store secret");
2250
+ console.error(pc10.dim(String(err)));
1396
2251
  process.exit(1);
1397
2252
  }
1398
- p5.outro(pc6.dim("Secret encrypted and saved to vault."));
2253
+ p8.outro(pc10.dim("Secret encrypted and saved to vault."));
1399
2254
  }
1400
2255
  });
1401
- var listCommand = defineCommand7({
2256
+ var listCommand2 = defineCommand11({
1402
2257
  meta: { name: "list", description: "List secret keys stored in the vault" },
1403
2258
  args: {
1404
2259
  server: {
@@ -1410,23 +2265,23 @@ var listCommand = defineCommand7({
1410
2265
  async run({ args }) {
1411
2266
  const results = listSecrets(args.server || void 0);
1412
2267
  if (results.length === 0) {
1413
- const filter = args.server ? ` for ${pc6.cyan(args.server)}` : "";
1414
- console.log(pc6.dim(`No secrets stored${filter}.`));
2268
+ const filter = args.server ? ` for ${pc10.cyan(args.server)}` : "";
2269
+ console.log(pc10.dim(`No secrets stored${filter}.`));
1415
2270
  return;
1416
2271
  }
1417
2272
  console.log("");
1418
2273
  for (const { server, keys } of results) {
1419
- console.log(pc6.bold(pc6.cyan(server)));
2274
+ console.log(pc10.bold(pc10.cyan(server)));
1420
2275
  for (const key of keys) {
1421
- console.log(` ${pc6.green("\u25CF")} ${pc6.bold(key)} ${pc6.dim(maskValue("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"))}`);
2276
+ console.log(` ${pc10.green("\u25CF")} ${pc10.bold(key)} ${pc10.dim(maskValue("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022"))}`);
1422
2277
  }
1423
2278
  console.log("");
1424
2279
  }
1425
2280
  const total = results.reduce((n, r) => n + r.keys.length, 0);
1426
- console.log(pc6.dim(` ${total} secret${total !== 1 ? "s" : ""} in ${results.length} server${results.length !== 1 ? "s" : ""}`));
2281
+ console.log(pc10.dim(` ${total} secret${total !== 1 ? "s" : ""} in ${results.length} server${results.length !== 1 ? "s" : ""}`));
1427
2282
  }
1428
2283
  });
1429
- var removeCommand = defineCommand7({
2284
+ var removeCommand = defineCommand11({
1430
2285
  meta: { name: "remove", description: "Delete a secret from the vault" },
1431
2286
  args: {
1432
2287
  server: {
@@ -1441,40 +2296,40 @@ var removeCommand = defineCommand7({
1441
2296
  }
1442
2297
  },
1443
2298
  async run({ args }) {
1444
- const confirmed = await p5.confirm({
1445
- message: `Remove ${pc6.bold(args.key)} from ${pc6.cyan(args.server)}?`,
2299
+ const confirmed = await p8.confirm({
2300
+ message: `Remove ${pc10.bold(args.key)} from ${pc10.cyan(args.server)}?`,
1446
2301
  initialValue: false
1447
2302
  });
1448
- if (p5.isCancel(confirmed) || !confirmed) {
1449
- p5.cancel("Cancelled.");
2303
+ if (p8.isCancel(confirmed) || !confirmed) {
2304
+ p8.cancel("Cancelled.");
1450
2305
  return;
1451
2306
  }
1452
2307
  try {
1453
2308
  removeSecret(args.server, args.key);
1454
- console.log(`${pc6.green("\u2713")} Removed ${pc6.bold(args.key)} from ${pc6.cyan(args.server)}`);
2309
+ console.log(`${pc10.green("\u2713")} Removed ${pc10.bold(args.key)} from ${pc10.cyan(args.server)}`);
1455
2310
  } catch (err) {
1456
- console.error(pc6.red("\u2717") + " Failed to remove secret");
1457
- console.error(pc6.dim(String(err)));
2311
+ console.error(pc10.red("\u2717") + " Failed to remove secret");
2312
+ console.error(pc10.dim(String(err)));
1458
2313
  process.exit(1);
1459
2314
  }
1460
2315
  }
1461
2316
  });
1462
- var secrets_default = defineCommand7({
2317
+ var secrets_default = defineCommand11({
1463
2318
  meta: {
1464
2319
  name: "secrets",
1465
2320
  description: "Manage encrypted secrets for MCP servers"
1466
2321
  },
1467
2322
  subCommands: {
1468
- set: setCommand,
1469
- list: listCommand,
2323
+ set: setCommand2,
2324
+ list: listCommand2,
1470
2325
  remove: removeCommand
1471
2326
  }
1472
2327
  });
1473
2328
 
1474
2329
  // src/commands/sync.ts
1475
- import { defineCommand as defineCommand8 } from "citty";
1476
- import * as p6 from "@clack/prompts";
1477
- import pc7 from "picocolors";
2330
+ import { defineCommand as defineCommand12 } from "citty";
2331
+ import * as p9 from "@clack/prompts";
2332
+ import pc11 from "picocolors";
1478
2333
 
1479
2334
  // src/core/config-diff.ts
1480
2335
  function reconstructServerEntry(lockEntry) {
@@ -1489,7 +2344,7 @@ function reconstructServerEntry(lockEntry) {
1489
2344
  }
1490
2345
  return entry;
1491
2346
  }
1492
- function computeDiff(lockfile, clientConfigs) {
2347
+ function computeDiff(lockfile, clientConfigs, options = {}) {
1493
2348
  const actions = [];
1494
2349
  for (const [server, lockEntry] of Object.entries(lockfile.servers)) {
1495
2350
  for (const client of lockEntry.clients) {
@@ -1507,19 +2362,21 @@ function computeDiff(lockfile, clientConfigs) {
1507
2362
  }
1508
2363
  }
1509
2364
  }
2365
+ const extraAction = options.remove ? "remove" : "extra";
1510
2366
  for (const [client, config] of clientConfigs) {
1511
2367
  for (const server of Object.keys(config.servers)) {
1512
2368
  if (!(server in lockfile.servers)) {
1513
- actions.push({ server, client, action: "extra" });
2369
+ actions.push({ server, client, action: extraAction });
1514
2370
  }
1515
2371
  }
1516
2372
  }
1517
2373
  return actions;
1518
2374
  }
1519
- function computeDiffFromClient(sourceClient, clientConfigs) {
2375
+ function computeDiffFromClient(sourceClient, clientConfigs, options = {}) {
1520
2376
  const actions = [];
1521
2377
  const sourceConfig = clientConfigs.get(sourceClient);
1522
2378
  if (!sourceConfig) return [];
2379
+ const extraAction = options.remove ? "remove" : "extra";
1523
2380
  for (const [client, config] of clientConfigs) {
1524
2381
  if (client === sourceClient) continue;
1525
2382
  for (const [server, entry] of Object.entries(sourceConfig.servers)) {
@@ -1531,7 +2388,7 @@ function computeDiffFromClient(sourceClient, clientConfigs) {
1531
2388
  }
1532
2389
  for (const server of Object.keys(config.servers)) {
1533
2390
  if (!(server in sourceConfig.servers)) {
1534
- actions.push({ server, client, action: "extra" });
2391
+ actions.push({ server, client, action: extraAction });
1535
2392
  }
1536
2393
  }
1537
2394
  }
@@ -1540,7 +2397,7 @@ function computeDiffFromClient(sourceClient, clientConfigs) {
1540
2397
 
1541
2398
  // src/core/sync-engine.ts
1542
2399
  async function applySyncActions(actions, clients) {
1543
- const result = { applied: 0, failed: 0, errors: [] };
2400
+ const result = { applied: 0, removed: 0, failed: 0, errors: [] };
1544
2401
  const addActions = actions.filter((a) => a.action === "add" && a.entry);
1545
2402
  for (const action of addActions) {
1546
2403
  const handler = clients.get(action.client);
@@ -1565,6 +2422,30 @@ async function applySyncActions(actions, clients) {
1565
2422
  });
1566
2423
  }
1567
2424
  }
2425
+ const removeActions = actions.filter((a) => a.action === "remove");
2426
+ for (const action of removeActions) {
2427
+ const handler = clients.get(action.client);
2428
+ if (!handler) {
2429
+ result.failed++;
2430
+ result.errors.push({
2431
+ server: action.server,
2432
+ client: action.client,
2433
+ error: "No handler available for client"
2434
+ });
2435
+ continue;
2436
+ }
2437
+ try {
2438
+ await handler.removeServer(action.server);
2439
+ result.removed++;
2440
+ } catch (err) {
2441
+ result.failed++;
2442
+ result.errors.push({
2443
+ server: action.server,
2444
+ client: action.client,
2445
+ error: String(err)
2446
+ });
2447
+ }
2448
+ }
1568
2449
  return result;
1569
2450
  }
1570
2451
  async function getClientConfigs() {
@@ -1598,7 +2479,7 @@ var CLIENT_DISPLAY3 = {
1598
2479
  vscode: "VS Code",
1599
2480
  windsurf: "Windsurf"
1600
2481
  };
1601
- var sync_default = defineCommand8({
2482
+ var sync_default = defineCommand12({
1602
2483
  meta: {
1603
2484
  name: "sync",
1604
2485
  description: "Sync MCP server configs across all detected AI clients"
@@ -1609,6 +2490,11 @@ var sync_default = defineCommand8({
1609
2490
  description: "Preview changes without applying them",
1610
2491
  default: false
1611
2492
  },
2493
+ remove: {
2494
+ type: "boolean",
2495
+ description: "Remove extra servers not in lockfile",
2496
+ default: false
2497
+ },
1612
2498
  source: {
1613
2499
  type: "string",
1614
2500
  description: "Use a specific client as source of truth (claude-desktop, cursor, vscode, windsurf)"
@@ -1620,58 +2506,64 @@ var sync_default = defineCommand8({
1620
2506
  }
1621
2507
  },
1622
2508
  async run({ args }) {
1623
- p6.intro(`${pc7.cyan("mcpman sync")}`);
2509
+ p9.intro(`${pc11.cyan("mcpman sync")}`);
1624
2510
  const sourceClient = args.source;
1625
2511
  if (sourceClient && !VALID_CLIENTS.includes(sourceClient)) {
1626
- p6.log.error(`Invalid --source "${sourceClient}". Must be one of: ${VALID_CLIENTS.join(", ")}`);
2512
+ p9.log.error(`Invalid --source "${sourceClient}". Must be one of: ${VALID_CLIENTS.join(", ")}`);
1627
2513
  process.exit(1);
1628
2514
  }
1629
- const spinner5 = p6.spinner();
2515
+ const spinner5 = p9.spinner();
1630
2516
  spinner5.start("Detecting clients and reading configs...");
1631
2517
  const { configs, handlers } = await getClientConfigs();
1632
2518
  spinner5.stop(`Found ${configs.size} client(s)`);
1633
2519
  if (configs.size === 0) {
1634
- p6.log.warn("No AI clients detected. Install Claude Desktop, Cursor, VS Code, or Windsurf first.");
2520
+ p9.log.warn("No AI clients detected. Install Claude Desktop, Cursor, VS Code, or Windsurf first.");
1635
2521
  process.exit(0);
1636
2522
  }
2523
+ const diffOptions = { remove: args.remove };
1637
2524
  let actions;
1638
2525
  if (sourceClient) {
1639
2526
  if (!configs.has(sourceClient)) {
1640
- p6.log.error(`Source client "${sourceClient}" is not detected or its config is unreadable.`);
2527
+ p9.log.error(`Source client "${sourceClient}" is not detected or its config is unreadable.`);
1641
2528
  process.exit(1);
1642
2529
  }
1643
- p6.log.info(`Using ${CLIENT_DISPLAY3[sourceClient]} as source of truth`);
1644
- actions = computeDiffFromClient(sourceClient, configs);
2530
+ p9.log.info(`Using ${CLIENT_DISPLAY3[sourceClient]} as source of truth`);
2531
+ actions = computeDiffFromClient(sourceClient, configs, diffOptions);
1645
2532
  } else {
1646
2533
  const lockfile = readLockfile();
1647
- actions = computeDiff(lockfile, configs);
2534
+ actions = computeDiff(lockfile, configs, diffOptions);
1648
2535
  }
1649
2536
  printDiffTable(actions);
1650
2537
  const addCount = actions.filter((a) => a.action === "add").length;
1651
2538
  const extraCount = actions.filter((a) => a.action === "extra").length;
1652
- if (addCount === 0 && extraCount === 0) {
1653
- p6.outro(pc7.green("All clients are in sync."));
2539
+ const removeCount = actions.filter((a) => a.action === "remove").length;
2540
+ if (addCount === 0 && removeCount === 0 && extraCount === 0) {
2541
+ p9.outro(pc11.green("All clients are in sync."));
1654
2542
  process.exit(0);
1655
2543
  }
1656
2544
  const parts = [];
1657
- if (addCount > 0) parts.push(pc7.green(`${addCount} to add`));
1658
- if (extraCount > 0) parts.push(pc7.yellow(`${extraCount} extra (informational)`));
1659
- p6.log.info(parts.join(" \xB7 "));
2545
+ if (addCount > 0) parts.push(pc11.green(`${addCount} to add`));
2546
+ if (removeCount > 0) parts.push(pc11.red(`${removeCount} to remove`));
2547
+ if (extraCount > 0) parts.push(pc11.yellow(`${extraCount} extra (informational)`));
2548
+ p9.log.info(parts.join(" \xB7 "));
1660
2549
  if (args["dry-run"]) {
1661
- p6.outro(pc7.dim("Dry run \u2014 no changes applied."));
2550
+ p9.outro(pc11.dim("Dry run \u2014 no changes applied."));
1662
2551
  process.exit(1);
1663
2552
  }
1664
- if (addCount === 0) {
1665
- p6.outro(pc7.dim("No additions needed. Extra servers left untouched."));
2553
+ if (addCount === 0 && removeCount === 0) {
2554
+ p9.outro(pc11.dim("No additions needed. Extra servers left untouched."));
1666
2555
  process.exit(1);
1667
2556
  }
1668
2557
  if (!args.yes) {
1669
- const confirmed = await p6.confirm({
1670
- message: `Apply ${addCount} addition(s) to client configs?`,
2558
+ const actionParts = [];
2559
+ if (addCount > 0) actionParts.push(`${addCount} addition(s)`);
2560
+ if (removeCount > 0) actionParts.push(`${removeCount} removal(s)`);
2561
+ const confirmed = await p9.confirm({
2562
+ message: `Apply ${actionParts.join(" and ")} to client configs?`,
1671
2563
  initialValue: true
1672
2564
  });
1673
- if (p6.isCancel(confirmed) || !confirmed) {
1674
- p6.outro(pc7.dim("Cancelled \u2014 no changes applied."));
2565
+ if (p9.isCancel(confirmed) || !confirmed) {
2566
+ p9.outro(pc11.dim("Cancelled \u2014 no changes applied."));
1675
2567
  process.exit(0);
1676
2568
  }
1677
2569
  }
@@ -1679,195 +2571,80 @@ var sync_default = defineCommand8({
1679
2571
  const result = await applySyncActions(actions, handlers);
1680
2572
  spinner5.stop("Done");
1681
2573
  if (result.applied > 0) {
1682
- p6.log.success(`Added ${result.applied} server(s) to client configs.`);
2574
+ p9.log.success(`Added ${result.applied} server(s) to client configs.`);
2575
+ }
2576
+ if (result.removed > 0) {
2577
+ p9.log.success(`Removed ${result.removed} server(s) from client configs.`);
1683
2578
  }
1684
2579
  if (result.failed > 0) {
1685
2580
  for (const e of result.errors) {
1686
- p6.log.error(`Failed to add "${e.server}" to ${e.client}: ${e.error}`);
2581
+ p9.log.error(`Failed to sync "${e.server}" on ${e.client}: ${e.error}`);
1687
2582
  }
1688
2583
  }
1689
- p6.outro(result.failed === 0 ? pc7.green("Sync complete.") : pc7.yellow("Sync complete with errors."));
2584
+ p9.outro(result.failed === 0 ? pc11.green("Sync complete.") : pc11.yellow("Sync complete with errors."));
1690
2585
  process.exit(result.failed > 0 ? 1 : 0);
1691
2586
  }
1692
2587
  });
1693
2588
  function printDiffTable(actions) {
1694
2589
  if (actions.length === 0) {
1695
- p6.log.info("No actions to display.");
2590
+ p9.log.info("No actions to display.");
1696
2591
  return;
1697
2592
  }
1698
2593
  const nameWidth = Math.max(6, ...actions.map((a) => a.server.length));
1699
2594
  const clientWidth = Math.max(6, ...actions.map((a) => CLIENT_DISPLAY3[a.client]?.length ?? a.client.length));
1700
- const header = ` ${pad2("SERVER", nameWidth)} ${pad2("CLIENT", clientWidth)} STATUS`;
1701
- console.log(pc7.dim(header));
1702
- console.log(pc7.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientWidth)} ------`));
2595
+ const header = ` ${pad3("SERVER", nameWidth)} ${pad3("CLIENT", clientWidth)} STATUS`;
2596
+ console.log(pc11.dim(header));
2597
+ console.log(pc11.dim(` ${"-".repeat(nameWidth)} ${"-".repeat(clientWidth)} ------`));
1703
2598
  for (const action of actions) {
1704
2599
  const clientDisplay = CLIENT_DISPLAY3[action.client] ?? action.client;
1705
2600
  const [icon, statusText] = formatAction(action.action);
1706
- console.log(` ${pad2(action.server, nameWidth)} ${pad2(clientDisplay, clientWidth)} ${icon} ${statusText}`);
2601
+ console.log(` ${pad3(action.server, nameWidth)} ${pad3(clientDisplay, clientWidth)} ${icon} ${statusText}`);
1707
2602
  }
1708
2603
  console.log("");
1709
2604
  }
1710
2605
  function formatAction(action) {
1711
2606
  switch (action) {
1712
2607
  case "add":
1713
- return [pc7.green("+"), pc7.green("missing \u2014 will add")];
2608
+ return [pc11.green("+"), pc11.green("missing \u2014 will add")];
1714
2609
  case "extra":
1715
- return [pc7.yellow("?"), pc7.yellow("extra (not in lockfile)")];
2610
+ return [pc11.yellow("?"), pc11.yellow("extra (not in lockfile)")];
2611
+ case "remove":
2612
+ return [pc11.red("\u2013"), pc11.red("extra \u2014 will remove")];
1716
2613
  case "ok":
1717
- return [pc7.dim("\xB7"), pc7.dim("in sync")];
2614
+ return [pc11.dim("\xB7"), pc11.dim("in sync")];
1718
2615
  }
1719
2616
  }
1720
- function pad2(s, width) {
2617
+ function pad3(s, width) {
1721
2618
  return s.length >= width ? s : s + " ".repeat(width - s.length);
1722
2619
  }
1723
2620
 
1724
2621
  // src/commands/update.ts
1725
- import { defineCommand as defineCommand9 } from "citty";
1726
- import * as p7 from "@clack/prompts";
1727
- import pc9 from "picocolors";
1728
-
1729
- // src/core/version-checker.ts
1730
- function compareVersions(a, b) {
1731
- const aParts = a.replace(/^v/, "").split(".").map(Number);
1732
- const bParts = b.replace(/^v/, "").split(".").map(Number);
1733
- const len = Math.max(aParts.length, bParts.length);
1734
- for (let i = 0; i < len; i++) {
1735
- const aN = aParts[i] ?? 0;
1736
- const bN = bParts[i] ?? 0;
1737
- if (Number.isNaN(aN) || Number.isNaN(bN)) return 0;
1738
- if (aN < bN) return -1;
1739
- if (aN > bN) return 1;
1740
- }
1741
- return 0;
1742
- }
1743
- function detectUpdateType(current, latest) {
1744
- const cParts = current.replace(/^v/, "").split(".").map(Number);
1745
- const lParts = latest.replace(/^v/, "").split(".").map(Number);
1746
- if ((lParts[0] ?? 0) > (cParts[0] ?? 0)) return "major";
1747
- if ((lParts[1] ?? 0) > (cParts[1] ?? 0)) return "minor";
1748
- return "patch";
1749
- }
1750
- async function fetchNpmLatest(packageName) {
1751
- try {
1752
- const res = await fetch(
1753
- `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`,
1754
- {
1755
- headers: { Accept: "application/json" },
1756
- signal: AbortSignal.timeout(8e3)
1757
- }
1758
- );
1759
- if (!res.ok) return null;
1760
- const data = await res.json();
1761
- return typeof data.version === "string" ? data.version : null;
1762
- } catch {
1763
- return null;
1764
- }
1765
- }
1766
- async function fetchSmitheryLatest(name) {
1767
- try {
1768
- const res = await fetch(
1769
- `https://registry.smithery.ai/servers/${encodeURIComponent(name)}`,
1770
- {
1771
- headers: { Accept: "application/json" },
1772
- signal: AbortSignal.timeout(8e3)
1773
- }
1774
- );
1775
- if (!res.ok) return null;
1776
- const data = await res.json();
1777
- return typeof data.version === "string" ? data.version : null;
1778
- } catch {
1779
- return null;
1780
- }
1781
- }
1782
- async function fetchGithubLatest(resolved) {
1783
- const match = resolved.match(/github\.com\/([^/]+)\/([^/]+)/);
1784
- if (!match) return null;
1785
- const [, owner, repo] = match;
1786
- try {
1787
- const res = await fetch(
1788
- `https://api.github.com/repos/${owner}/${repo}/releases/latest`,
1789
- {
1790
- headers: { Accept: "application/json" },
1791
- signal: AbortSignal.timeout(8e3)
1792
- }
1793
- );
1794
- if (!res.ok) return null;
1795
- const data = await res.json();
1796
- return typeof data.tag_name === "string" ? data.tag_name.replace(/^v/, "") : null;
1797
- } catch {
1798
- return null;
1799
- }
1800
- }
1801
- async function checkVersion(name, lockEntry) {
1802
- const current = lockEntry.version;
1803
- let latest = null;
1804
- if (lockEntry.source === "npm") {
1805
- latest = await fetchNpmLatest(name);
1806
- } else if (lockEntry.source === "smithery") {
1807
- latest = await fetchSmitheryLatest(name);
1808
- } else if (lockEntry.source === "github") {
1809
- latest = await fetchGithubLatest(lockEntry.resolved);
1810
- }
1811
- if (!latest || latest === current) {
1812
- return {
1813
- server: name,
1814
- source: lockEntry.source,
1815
- currentVersion: current,
1816
- latestVersion: latest ?? current,
1817
- hasUpdate: false
1818
- };
1819
- }
1820
- const hasUpdate = compareVersions(current, latest) === -1;
1821
- return {
1822
- server: name,
1823
- source: lockEntry.source,
1824
- currentVersion: current,
1825
- latestVersion: latest,
1826
- hasUpdate,
1827
- updateType: hasUpdate ? detectUpdateType(current, latest) : void 0
1828
- };
1829
- }
1830
- async function checkAllVersions(lockfile) {
1831
- const entries = Object.entries(lockfile.servers);
1832
- if (entries.length === 0) return [];
1833
- const results = [];
1834
- const executing = /* @__PURE__ */ new Set();
1835
- for (const [name, entry] of entries) {
1836
- const p8 = checkVersion(name, entry).then((r) => {
1837
- results.push(r);
1838
- executing.delete(p8);
1839
- });
1840
- executing.add(p8);
1841
- if (executing.size >= 5) {
1842
- await Promise.race(executing);
1843
- }
1844
- }
1845
- await Promise.all(executing);
1846
- return results;
1847
- }
2622
+ import { defineCommand as defineCommand13 } from "citty";
2623
+ import * as p10 from "@clack/prompts";
2624
+ import pc13 from "picocolors";
1848
2625
 
1849
2626
  // src/core/update-notifier.ts
1850
- import fs3 from "fs";
1851
- import path4 from "path";
2627
+ import fs4 from "fs";
2628
+ import path5 from "path";
1852
2629
  import os3 from "os";
1853
- import pc8 from "picocolors";
1854
- var CACHE_FILE = path4.join(os3.homedir(), ".mcpman", ".update-check");
2630
+ import pc12 from "picocolors";
2631
+ var CACHE_FILE = path5.join(os3.homedir(), ".mcpman", ".update-check");
1855
2632
  var TTL_MS = 24 * 60 * 60 * 1e3;
1856
2633
  function writeUpdateCache(data) {
1857
2634
  try {
1858
- const dir = path4.dirname(CACHE_FILE);
1859
- if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
2635
+ const dir = path5.dirname(CACHE_FILE);
2636
+ if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
1860
2637
  const tmp = `${CACHE_FILE}.tmp`;
1861
- fs3.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
1862
- fs3.renameSync(tmp, CACHE_FILE);
2638
+ fs4.writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
2639
+ fs4.renameSync(tmp, CACHE_FILE);
1863
2640
  } catch {
1864
2641
  }
1865
2642
  }
1866
2643
 
1867
2644
  // src/commands/update.ts
1868
- async function loadClients2() {
2645
+ async function loadClients3() {
1869
2646
  try {
1870
- const mod = await import("./client-detector-SUIJSIYM.js");
2647
+ const mod = await import("./client-detector-UAP2EYZA.js");
1871
2648
  return mod.getInstalledClients();
1872
2649
  } catch {
1873
2650
  return [];
@@ -1882,19 +2659,19 @@ function printTable(updates) {
1882
2659
  "LATEST".padEnd(VER_W),
1883
2660
  "STATUS"
1884
2661
  ].join(" ");
1885
- console.log(pc9.bold(`
2662
+ console.log(pc13.bold(`
1886
2663
  ${header}`));
1887
- console.log(pc9.dim(` ${"\u2500".repeat(NAME_W + VER_W * 2 + 20)}`));
2664
+ console.log(pc13.dim(` ${"\u2500".repeat(NAME_W + VER_W * 2 + 20)}`));
1888
2665
  for (const u of updates) {
1889
2666
  const nameCol = u.server.slice(0, NAME_W).padEnd(NAME_W);
1890
2667
  const curCol = u.currentVersion.padEnd(VER_W);
1891
2668
  const latCol = u.latestVersion.padEnd(VER_W);
1892
- const statusCol = u.hasUpdate ? pc9.yellow(`Update available${u.updateType ? ` [${u.updateType}]` : ""}`) : pc9.green("Up to date");
2669
+ const statusCol = u.hasUpdate ? pc13.yellow(`Update available${u.updateType ? ` [${u.updateType}]` : ""}`) : pc13.green("Up to date");
1893
2670
  console.log(` ${nameCol} ${curCol} ${latCol} ${statusCol}`);
1894
2671
  }
1895
2672
  console.log();
1896
2673
  }
1897
- var update_default = defineCommand9({
2674
+ var update_default = defineCommand13({
1898
2675
  meta: {
1899
2676
  name: "update",
1900
2677
  description: "Check for and apply updates to installed MCP servers"
@@ -1933,7 +2710,7 @@ var update_default = defineCommand9({
1933
2710
  }
1934
2711
  process.exit(1);
1935
2712
  }
1936
- const spinner5 = p7.spinner();
2713
+ const spinner5 = p10.spinner();
1937
2714
  spinner5.start("Checking versions...");
1938
2715
  let updates;
1939
2716
  try {
@@ -1955,70 +2732,50 @@ var update_default = defineCommand9({
1955
2732
  printTable(updates);
1956
2733
  const outdated = updates.filter((u) => u.hasUpdate);
1957
2734
  if (outdated.length === 0) {
1958
- console.log(pc9.green(" All servers are up to date."));
2735
+ console.log(pc13.green(" All servers are up to date."));
1959
2736
  return;
1960
2737
  }
1961
2738
  if (args.check) {
1962
- console.log(pc9.yellow(` ${outdated.length} update(s) available. Run mcpman update to apply.`));
2739
+ console.log(pc13.yellow(` ${outdated.length} update(s) available. Run mcpman update to apply.`));
1963
2740
  return;
1964
2741
  }
1965
2742
  if (!args.yes) {
1966
- const confirmed = await p7.confirm({
2743
+ const confirmed = await p10.confirm({
1967
2744
  message: `Apply ${outdated.length} update(s)?`,
1968
2745
  initialValue: true
1969
2746
  });
1970
- if (p7.isCancel(confirmed) || !confirmed) {
1971
- p7.outro("Cancelled.");
2747
+ if (p10.isCancel(confirmed) || !confirmed) {
2748
+ p10.outro("Cancelled.");
1972
2749
  return;
1973
2750
  }
1974
2751
  }
1975
- const clients = await loadClients2();
2752
+ const clients = await loadClients3();
1976
2753
  let successCount = 0;
1977
2754
  for (const update of outdated) {
1978
- const lockEntry = servers[update.server];
1979
- const input = lockEntry.source === "smithery" ? `smithery:${update.server}` : lockEntry.source === "github" ? lockEntry.resolved : update.server;
1980
- const s = p7.spinner();
2755
+ const s = p10.spinner();
1981
2756
  s.start(`Updating ${update.server}...`);
1982
- try {
1983
- const metadata = await resolveServer(input);
1984
- const integrity = computeIntegrity(metadata.resolved);
1985
- addEntry(update.server, {
1986
- ...lockEntry,
1987
- version: metadata.version,
1988
- resolved: metadata.resolved,
1989
- integrity,
1990
- command: metadata.command,
1991
- args: metadata.args,
1992
- installedAt: (/* @__PURE__ */ new Date()).toISOString()
1993
- });
1994
- const entryClients = clients.filter(
1995
- (c) => lockEntry.clients.includes(c.type)
1996
- );
1997
- for (const client of entryClients) {
1998
- try {
1999
- await client.addServer(update.server, {
2000
- command: metadata.command,
2001
- args: metadata.args
2002
- });
2003
- } catch {
2004
- }
2005
- }
2006
- s.stop(`${pc9.green("\u2713")} ${update.server}: ${update.currentVersion} \u2192 ${metadata.version}`);
2757
+ const result = await applyServerUpdate(
2758
+ update.server,
2759
+ servers[update.server],
2760
+ clients
2761
+ );
2762
+ if (result.success) {
2763
+ s.stop(`${pc13.green("\u2713")} ${update.server}: ${result.fromVersion} \u2192 ${result.toVersion}`);
2007
2764
  successCount++;
2008
- } catch (err) {
2009
- s.stop(`${pc9.red("\u2717")} ${update.server}: ${err instanceof Error ? err.message : String(err)}`);
2765
+ } else {
2766
+ s.stop(`${pc13.red("\u2717")} ${update.server}: ${result.error}`);
2010
2767
  }
2011
2768
  }
2012
2769
  const freshLockfile = readLockfile(resolveLockfilePath());
2013
2770
  const freshUpdates = await checkAllVersions(freshLockfile);
2014
2771
  writeUpdateCache({ lastCheck: (/* @__PURE__ */ new Date()).toISOString(), updates: freshUpdates });
2015
- p7.outro(`${successCount} of ${outdated.length} server(s) updated.`);
2772
+ p10.outro(`${successCount} of ${outdated.length} server(s) updated.`);
2016
2773
  }
2017
2774
  });
2018
2775
 
2019
2776
  // src/utils/constants.ts
2020
2777
  var APP_NAME = "mcpman";
2021
- var APP_VERSION = "0.2.0";
2778
+ var APP_VERSION = "0.4.0";
2022
2779
  var APP_DESCRIPTION = "The package manager for MCP servers";
2023
2780
 
2024
2781
  // src/index.ts
@@ -2026,7 +2783,7 @@ process.on("SIGINT", () => {
2026
2783
  console.log("\nAborted.");
2027
2784
  process.exit(130);
2028
2785
  });
2029
- var main = defineCommand10({
2786
+ var main = defineCommand14({
2030
2787
  meta: {
2031
2788
  name: APP_NAME,
2032
2789
  version: APP_VERSION,
@@ -2041,7 +2798,11 @@ var main = defineCommand10({
2041
2798
  secrets: secrets_default,
2042
2799
  sync: sync_default,
2043
2800
  audit: audit_default,
2044
- update: update_default
2801
+ update: update_default,
2802
+ config: config_default,
2803
+ search: search_default,
2804
+ info: info_default,
2805
+ run: run_default
2045
2806
  }
2046
2807
  });
2047
2808
  runMain(main);