devmap 0.3.8__py3-none-any.whl → 0.3.9__py3-none-any.whl

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.
devmap/_devmap.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  // @bun
3
3
 
4
4
  // src/version.ts
5
- var VERSION = "0.3.8";
5
+ var VERSION = "0.3.9";
6
6
 
7
7
  // src/install.ts
8
8
  function isIsolated(i) {
@@ -662,6 +662,206 @@ function runScaAudit(cwd) {
662
662
  return checks;
663
663
  }
664
664
 
665
+ // src/security/secrets.ts
666
+ import { existsSync as existsSync2, readFileSync, statSync as statSync2, readdirSync as readdirSync2 } from "node:fs";
667
+ import { homedir } from "node:os";
668
+ import { join as join2, relative, isAbsolute } from "node:path";
669
+ var RULES = [
670
+ { name: "AWS Access Key ID", re: /\b(AKIA[0-9A-Z]{16})\b/g, capture: true },
671
+ { name: "AWS Secret Access Key", re: /aws_secret_access_key\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/gi, capture: true },
672
+ { name: "GitHub Token", re: /\b(ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{82})\b/g, capture: true },
673
+ { name: "GitLab Personal Access Token", re: /\bglpat-[A-Za-z0-9_-]{20}\b/g, capture: true },
674
+ { name: "Slack Token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, capture: true },
675
+ {
676
+ name: "Slack Incoming Webhook",
677
+ re: /https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9/]+/g,
678
+ capture: false,
679
+ mask: "https://hooks.slack.com/services/***"
680
+ },
681
+ { name: "Stripe Secret Key", re: /\bsk_live_[A-Za-z0-9]{24,}\b/g, capture: true },
682
+ { name: "OpenAI API Key", re: /\bsk-[A-Za-z0-9]{20,}\b/g, capture: true },
683
+ { name: "GCP API Key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g, capture: true },
684
+ { name: "Google OAuth Client Secret", re: /"client_secret"\s*:\s*"([A-Za-z0-9-_]{20,})"/g, capture: true },
685
+ {
686
+ name: "Private Key Block",
687
+ re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/g,
688
+ capture: false,
689
+ mask: "[PRIVATE KEY BLOCK]"
690
+ },
691
+ { name: "Bearer Token", re: /\bBearer\s+([A-Za-z0-9._-]{20,})\b/g, capture: true },
692
+ { name: "Database URL with password", re: /(?:postgres|postgresql|mysql|mongodb(?:\+srv)?):\/\/[^:/\s]+:([^@\s]+)@/gi, capture: true },
693
+ { name: "npm authToken", re: /\/\/registry\.npmjs\.org\/:_authToken=([A-Za-z0-9_]{36,})/g, capture: true },
694
+ {
695
+ name: "Git URL with embedded credentials",
696
+ re: /[a-z]+:\/\/([^\s:@/]+):([^\s:@/]+)@/gi,
697
+ capture: true,
698
+ captureGroup: 2
699
+ },
700
+ {
701
+ name: "Generic Secret Assignment",
702
+ re: /(?:api[_-]?key|apikey|secret[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|private[_-]?key|secret|passwd|password|token)\s*[=:]\s*['"]?([A-Za-z0-9/+=_\-]{8,64})['"]?/gi,
703
+ capture: true,
704
+ generic: true
705
+ }
706
+ ];
707
+ function redactText(v) {
708
+ if (v.length <= 4)
709
+ return "*".repeat(v.length);
710
+ return v.slice(0, 4) + "****" + (v.length > 8 ? " (len " + v.length + ")" : "");
711
+ }
712
+ function looksSecret(v) {
713
+ if (v.length >= 32)
714
+ return true;
715
+ if (v.length < 12)
716
+ return false;
717
+ let classes = 0;
718
+ if (/[a-z]/.test(v))
719
+ classes++;
720
+ if (/[A-Z]/.test(v))
721
+ classes++;
722
+ if (/[0-9]/.test(v))
723
+ classes++;
724
+ if (/[^A-Za-z0-9]/.test(v))
725
+ classes++;
726
+ return classes >= 3;
727
+ }
728
+ function slug(s) {
729
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
730
+ }
731
+ function toRel(p) {
732
+ const home = homedir();
733
+ const lower = p.replace(/\\/g, "/");
734
+ const homeLower = home.replace(/\\/g, "/");
735
+ if (lower.startsWith(homeLower))
736
+ return "~" + lower.slice(homeLower.length);
737
+ try {
738
+ const r = relative(process.cwd(), p).replace(/\\/g, "/");
739
+ if (!r.startsWith("..") && !isAbsolute(r))
740
+ return r;
741
+ } catch {}
742
+ return p;
743
+ }
744
+ function discoverFiles(projectRoots, includeHome) {
745
+ const home = homedir();
746
+ const out = [];
747
+ const seen = new Set;
748
+ const add = (p) => {
749
+ try {
750
+ if (existsSync2(p) && statSync2(p).isFile()) {
751
+ const rp = p.replace(/\\/g, "/");
752
+ if (!seen.has(rp)) {
753
+ seen.add(rp);
754
+ out.push(p);
755
+ }
756
+ }
757
+ } catch {}
758
+ };
759
+ if (includeHome) {
760
+ const homeFiles = [
761
+ ".env",
762
+ ".bashrc",
763
+ ".zshrc",
764
+ ".bash_profile",
765
+ ".profile",
766
+ ".npmrc",
767
+ ".pypirc",
768
+ ".netrc",
769
+ ".git-credentials",
770
+ ".gitconfig",
771
+ ".docker/config.json",
772
+ ".kube/config",
773
+ ".aws/credentials",
774
+ ".config/gcloud/application_default_credentials.json",
775
+ ".azure/accessTokens.json",
776
+ ".ssh/id_rsa",
777
+ ".ssh/id_ecdsa",
778
+ ".ssh/id_ed25519",
779
+ ".ssh/id_dsa",
780
+ ".envrc"
781
+ ];
782
+ for (const f of homeFiles)
783
+ add(join2(home, f));
784
+ try {
785
+ for (const e of readdirSync2(home)) {
786
+ if (e.startsWith(".env") && e !== ".env")
787
+ add(join2(home, e));
788
+ }
789
+ } catch {}
790
+ }
791
+ const roots = [...new Set([process.cwd(), ...projectRoots])];
792
+ for (const root of roots) {
793
+ let entries = [];
794
+ try {
795
+ entries = readdirSync2(root);
796
+ } catch {
797
+ continue;
798
+ }
799
+ for (const e of entries) {
800
+ if (e === ".env" || e.startsWith(".env.") || e === ".envrc" || e === ".npmrc" || e === ".pypirc" || e === ".netrc" || e === ".gitconfig" || e === "credentials.json" || e === "secrets.json" || e.endsWith(".pem") || e.endsWith(".key") || e.endsWith(".p12") || e.endsWith(".pfx") || /^service-?account.*\.json$/i.test(e)) {
801
+ add(join2(root, e));
802
+ }
803
+ }
804
+ add(join2(root, ".git", "config"));
805
+ }
806
+ return out;
807
+ }
808
+ function scanSecrets(projectRoots = [], includeHome = true) {
809
+ const files = discoverFiles(projectRoots, includeHome);
810
+ const checks = [];
811
+ let scanned = 0;
812
+ for (const f of files) {
813
+ let content;
814
+ try {
815
+ const st = statSync2(f);
816
+ if (!st.isFile() || st.size > 512 * 1024)
817
+ continue;
818
+ content = readFileSync(f, "utf8");
819
+ } catch {
820
+ continue;
821
+ }
822
+ scanned++;
823
+ const lines = content.split(/\r?\n/);
824
+ for (let i = 0;i < lines.length; i++) {
825
+ const line = lines[i];
826
+ const accepted = [];
827
+ for (const rule of RULES) {
828
+ rule.re.lastIndex = 0;
829
+ let m;
830
+ while ((m = rule.re.exec(line)) !== null) {
831
+ if (m.index === rule.re.lastIndex)
832
+ rule.re.lastIndex++;
833
+ const start = m.index;
834
+ const end = start + m[0].length;
835
+ if (rule.generic === true) {
836
+ const val = (rule.captureGroup ? m[rule.captureGroup] : m[1]) || m[0];
837
+ if (!looksSecret(val))
838
+ continue;
839
+ if (accepted.some(([s, e]) => start < e && end > s))
840
+ continue;
841
+ }
842
+ const masked = redactValue(rule, m);
843
+ const rel = toRel(f);
844
+ const id = `security.secrets.${slug(rule.name)}.${checks.length + 1}`;
845
+ checks.push(makeCheck(id, "secret", "warn", `${rule.name} in ${rel}:${i + 1} (${masked})`, `${rule.name} detected at ${f}:${i + 1}. The value is redacted (${masked}) and never printed in full. ` + `Rotate or remove the credential and prefer a secret manager or build-time env injection.`, [f]));
846
+ accepted.push([start, end]);
847
+ }
848
+ }
849
+ }
850
+ }
851
+ if (checks.length === 0) {
852
+ checks.push(makeCheck("security.secrets.ok", "secret", "info", `No hardcoded secrets found in ${scanned} scanned config file(s)`, `Scanned ${scanned} credential/config file(s) under HOME and project roots (opt-in --scan-secrets). ` + `No known secret patterns matched.`, files.slice(0, 20)));
853
+ }
854
+ return checks;
855
+ }
856
+ function redactValue(rule, m) {
857
+ if (rule.capture) {
858
+ const g = m[rule.captureGroup ?? 1];
859
+ if (g)
860
+ return redactText(g);
861
+ }
862
+ return rule.mask ?? "[redacted]";
863
+ }
864
+
665
865
  // src/health/parse.ts
666
866
  function parsePipCheck(text) {
667
867
  const lines = (text || "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
@@ -1092,7 +1292,8 @@ async function runScan(shells, disabled, opts = {}) {
1092
1292
  const securityChecks = [
1093
1293
  ...computeSecurity(byName),
1094
1294
  ...computeScaInventory(),
1095
- ...opts.auditDeps ? runScaAudit(process.cwd()) : []
1295
+ ...opts.auditDeps ? runScaAudit(process.cwd()) : [],
1296
+ ...opts.scanSecrets ? scanSecrets(opts.projectRoots) : []
1096
1297
  ];
1097
1298
  const collected = [...allChecks, ...conflicts.checks, ...securityChecks, ...healthChecks].filter((c) => !isCheckDisabled(c.ID, disabled));
1098
1299
  const environments = {
@@ -1171,6 +1372,169 @@ function queryManifest(m, opts) {
1171
1372
  return { groups, checks, conflicts };
1172
1373
  }
1173
1374
 
1375
+ // src/mcp.ts
1376
+ var ALL_SHELLS = ["current", "powershell", "gitbash", "wsl"];
1377
+ function parseShells(v) {
1378
+ if (typeof v !== "string" || !v)
1379
+ return ["current"];
1380
+ const list = v.split(",").map((s) => s.trim());
1381
+ return list.includes("all") ? ALL_SHELLS : list.length ? list : ["current"];
1382
+ }
1383
+ function parseDisable(v) {
1384
+ if (typeof v !== "string" || !v)
1385
+ return new Set;
1386
+ return new Set(v.split(",").map((s) => s.trim()).filter(Boolean));
1387
+ }
1388
+ var TOOLS = [
1389
+ {
1390
+ name: "scan_environment",
1391
+ description: "Run a full devmap scan of the local development environment and return the structured manifest " + "(detected tools across runtimes/pkg-managers/IDEs, PATH conflicts & ghost binaries, per-env health " + "checks, and the security posture). Use this first to understand a machine before proposing changes.",
1392
+ inputSchema: {
1393
+ type: "object",
1394
+ properties: {
1395
+ shells: {
1396
+ type: "string",
1397
+ description: "Comma-separated shell views to reconstruct: current|powershell|gitbash|wsl|all (default current)"
1398
+ },
1399
+ disable: {
1400
+ type: "string",
1401
+ description: "Comma-separated check-rule prefixes to skip, e.g. 'path.duplicate,health.conda.base'"
1402
+ },
1403
+ auditDeps: {
1404
+ type: "boolean",
1405
+ description: "Opt-in dependency CVE audit via installed SCA tools (OSV-Scanner/pip-audit/govulncheck/Trivy). Uses network."
1406
+ },
1407
+ healthNetwork: {
1408
+ type: "boolean",
1409
+ description: "Also run network health probes (npm doctor / rustup check). Off by default to stay offline-fast."
1410
+ },
1411
+ scanSecrets: {
1412
+ type: "boolean",
1413
+ description: "Opt-in: scan a curated allowlist of HOME/project credential files for hardcoded secrets (offline)."
1414
+ }
1415
+ }
1416
+ }
1417
+ },
1418
+ {
1419
+ name: "query_tool",
1420
+ description: "Query a scan for a specific tool, semantic category, conflict, or issue — e.g. selector 'node', " + "'runtime.node', 'pkg.npm', 'git', or 'python'. Returns matching installs plus any related findings. " + "Cheaper than a full scan when you already know what you are looking for.",
1421
+ inputSchema: {
1422
+ type: "object",
1423
+ properties: {
1424
+ selector: {
1425
+ type: "string",
1426
+ description: "Tool id substring or 'category.tool' dotted form (e.g. node / runtime.node / pkg.npm)"
1427
+ },
1428
+ issuesOnly: { type: "boolean", description: "Only return issues (warn/error/missing) for the match" },
1429
+ conflictsOnly: { type: "boolean", description: "Only return PATH/version/ghost conflicts for the match" },
1430
+ offPathOnly: { type: "boolean", description: "Only return isolated off-PATH installs (version-manager envs excluded)" },
1431
+ shells: { type: "string", description: "Shell views (see scan_environment)" },
1432
+ disable: { type: "string", description: "Check-rule prefixes to skip (see scan_environment)" }
1433
+ }
1434
+ }
1435
+ }
1436
+ ];
1437
+ async function callTool(name, args) {
1438
+ if (name === "scan_environment") {
1439
+ const manifest = await runScan(parseShells(args.shells), parseDisable(args.disable), {
1440
+ silent: true,
1441
+ auditDeps: args.auditDeps === true,
1442
+ healthNetwork: args.healthNetwork === true,
1443
+ scanSecrets: args.scanSecrets === true
1444
+ });
1445
+ return { content: [{ type: "text", text: JSON.stringify(manifest, null, 2) }], isError: false };
1446
+ }
1447
+ if (name === "query_tool") {
1448
+ const manifest = await runScan(parseShells(args.shells), parseDisable(args.disable), { silent: true });
1449
+ const result = queryManifest(manifest, {
1450
+ selector: typeof args.selector === "string" ? args.selector : undefined,
1451
+ issues: args.issuesOnly === true,
1452
+ conflicts: args.conflictsOnly === true,
1453
+ offPath: args.offPathOnly === true
1454
+ });
1455
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }], isError: false };
1456
+ }
1457
+ return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
1458
+ }
1459
+ async function startMcpServer() {
1460
+ const toErr = (...a) => process.stderr.write(a.map((x) => typeof x === "string" ? x : String(x)).join(" ") + `
1461
+ `);
1462
+ console.log = toErr;
1463
+ console.error = toErr;
1464
+ console.warn = toErr;
1465
+ console.info = toErr;
1466
+ const write = (obj) => process.stdout.write(JSON.stringify(obj) + `
1467
+ `);
1468
+ const log = toErr;
1469
+ let buffer = "";
1470
+ process.stdin.setEncoding("utf8");
1471
+ for await (const chunk of process.stdin) {
1472
+ buffer += chunk;
1473
+ let nl;
1474
+ while ((nl = buffer.indexOf(`
1475
+ `)) >= 0) {
1476
+ const line = buffer.slice(0, nl).trim();
1477
+ buffer = buffer.slice(nl + 1);
1478
+ if (!line)
1479
+ continue;
1480
+ let msg;
1481
+ try {
1482
+ msg = JSON.parse(line);
1483
+ } catch {
1484
+ log("[mcp] dropping malformed frame");
1485
+ continue;
1486
+ }
1487
+ const id = msg.id;
1488
+ const method = msg.method;
1489
+ log("[mcp] ->", method || "?", id ?? "");
1490
+ if (!method) {
1491
+ if (id !== undefined)
1492
+ write({ jsonrpc: "2.0", id, error: { code: -32600, message: "missing method" } });
1493
+ continue;
1494
+ }
1495
+ switch (method) {
1496
+ case "initialize":
1497
+ write({
1498
+ jsonrpc: "2.0",
1499
+ id,
1500
+ result: {
1501
+ protocolVersion: msg.params?.protocolVersion || "2024-11-05",
1502
+ capabilities: { tools: {} },
1503
+ serverInfo: { name: "devmap", version: VERSION }
1504
+ }
1505
+ });
1506
+ break;
1507
+ case "notifications/initialized":
1508
+ case "initialized":
1509
+ case "notifications/cancelled":
1510
+ break;
1511
+ case "ping":
1512
+ write({ jsonrpc: "2.0", id, result: {} });
1513
+ break;
1514
+ case "tools/list":
1515
+ write({ jsonrpc: "2.0", id, result: { tools: TOOLS } });
1516
+ break;
1517
+ case "tools/call":
1518
+ try {
1519
+ const r = await callTool(msg.params?.name, msg.params?.arguments || {});
1520
+ write({ jsonrpc: "2.0", id, result: r });
1521
+ } catch (e) {
1522
+ write({
1523
+ jsonrpc: "2.0",
1524
+ id,
1525
+ result: { content: [{ type: "text", text: "error: " + (e?.message || String(e)) }], isError: true }
1526
+ });
1527
+ }
1528
+ break;
1529
+ default:
1530
+ if (id !== undefined)
1531
+ write({ jsonrpc: "2.0", id, error: { code: -32601, message: `method not found: ${method}` } });
1532
+ }
1533
+ }
1534
+ }
1535
+ log("[mcp] stdin closed, shutting down");
1536
+ }
1537
+
1174
1538
  // src/diff.ts
1175
1539
  var CAT_KEYS = [
1176
1540
  ["runtimes", "runtime"],
@@ -1264,9 +1628,9 @@ register({
1264
1628
  });
1265
1629
 
1266
1630
  // src/detector/runtimes.ts
1267
- import { homedir } from "node:os";
1268
- import { join as join2 } from "node:path";
1269
- import { existsSync as existsSync2, readdirSync as readdirSync2 } from "node:fs";
1631
+ import { homedir as homedir2 } from "node:os";
1632
+ import { join as join3 } from "node:path";
1633
+ import { existsSync as existsSync3, readdirSync as readdirSync3 } from "node:fs";
1270
1634
 
1271
1635
  // src/detector/util.ts
1272
1636
  var CONCURRENCY = 12;
@@ -1336,27 +1700,27 @@ async function detectSimpleAsync(list, onProgress) {
1336
1700
  }
1337
1701
 
1338
1702
  // src/detector/runtimes.ts
1339
- var HOME = homedir();
1703
+ var HOME = homedir2();
1340
1704
  async function condaPythonPaths() {
1341
1705
  const found = [];
1342
1706
  const bases = new Set;
1343
1707
  const ci = await runAsync("conda", ["info", "--base"], 15000);
1344
1708
  if (ci)
1345
1709
  bases.add(ci.trim());
1346
- bases.add(join2(HOME, "miniconda3"));
1347
- bases.add(join2(HOME, "anaconda3"));
1710
+ bases.add(join3(HOME, "miniconda3"));
1711
+ bases.add(join3(HOME, "anaconda3"));
1348
1712
  bases.add("E:\\Users\\Administrator\\miniconda3");
1349
1713
  for (const base of bases) {
1350
- if (!base || !existsSync2(base))
1714
+ if (!base || !existsSync3(base))
1351
1715
  continue;
1352
- const basePy = IS_WIN ? join2(base, "python.exe") : join2(base, "bin", "python");
1353
- if (existsSync2(basePy))
1716
+ const basePy = IS_WIN ? join3(base, "python.exe") : join3(base, "bin", "python");
1717
+ if (existsSync3(basePy))
1354
1718
  found.push({ path: basePy, env: "base" });
1355
- const envsDir = join2(base, "envs");
1356
- if (existsSync2(envsDir)) {
1357
- for (const e of readdirSync2(envsDir)) {
1358
- const py = IS_WIN ? join2(envsDir, e, "python.exe") : join2(envsDir, e, "bin", "python");
1359
- if (existsSync2(py))
1719
+ const envsDir = join3(base, "envs");
1720
+ if (existsSync3(envsDir)) {
1721
+ for (const e of readdirSync3(envsDir)) {
1722
+ const py = IS_WIN ? join3(envsDir, e, "python.exe") : join3(envsDir, e, "bin", "python");
1723
+ if (existsSync3(py))
1360
1724
  found.push({ path: py, env: e });
1361
1725
  }
1362
1726
  }
@@ -1369,14 +1733,14 @@ async function scanCondaPythons() {
1369
1733
  return paths.map((p, i) => ({ path: p.path, onPath: false, version: versions[i], env: p.env }));
1370
1734
  }
1371
1735
  async function scanNvmNodes() {
1372
- const cands = [join2(HOME, "AppData", "Roaming", "nvm"), join2(HOME, ".nvm", "versions", "node")];
1736
+ const cands = [join3(HOME, "AppData", "Roaming", "nvm"), join3(HOME, ".nvm", "versions", "node")];
1373
1737
  const found = [];
1374
1738
  for (const dir of cands) {
1375
- if (!existsSync2(dir))
1739
+ if (!existsSync3(dir))
1376
1740
  continue;
1377
- for (const v of readdirSync2(dir)) {
1378
- const bin = IS_WIN ? join2(dir, v, "node.exe") : join2(dir, v, "bin", "node");
1379
- if (existsSync2(bin))
1741
+ for (const v of readdirSync3(dir)) {
1742
+ const bin = IS_WIN ? join3(dir, v, "node.exe") : join3(dir, v, "bin", "node");
1743
+ if (existsSync3(bin))
1380
1744
  found.push({ path: bin, env: v });
1381
1745
  }
1382
1746
  }
@@ -1384,7 +1748,7 @@ async function scanNvmNodes() {
1384
1748
  return found.map((f, i) => ({ path: f.path, onPath: false, version: versions[i], env: f.env }));
1385
1749
  }
1386
1750
  async function scanKnownBinary(cands, flags) {
1387
- const hits = cands.filter((c) => existsSync2(c));
1751
+ const hits = cands.filter((c) => existsSync3(c));
1388
1752
  const versions = await pool(hits, 12, (c) => versionOfAsync(c, flags));
1389
1753
  return hits.map((c, i) => ({ path: c, onPath: false, version: versions[i] }));
1390
1754
  }
@@ -1399,7 +1763,7 @@ register({
1399
1763
  detectRuntimeAsync("rustc", "rustc", ["--version"]),
1400
1764
  detectRuntimeAsync("java", "java", ["-version"]),
1401
1765
  detectRuntimeAsync("ruby", "ruby", ["--version"]),
1402
- detectRuntimeAsync("bun", "bun", ["--version"], () => scanKnownBinary([join2(HOME, ".bun", "bin", "bun"), join2(HOME, ".bun", "bin", "bun.exe")], ["--version"]))
1766
+ detectRuntimeAsync("bun", "bun", ["--version"], () => scanKnownBinary([join3(HOME, ".bun", "bin", "bun"), join3(HOME, ".bun", "bin", "bun.exe")], ["--version"]))
1403
1767
  ]);
1404
1768
  const groups = results.filter((g) => g !== null);
1405
1769
  return { data: groups, checks: [] };
@@ -1407,7 +1771,7 @@ register({
1407
1771
  });
1408
1772
 
1409
1773
  // src/detector/pkgmgrs.ts
1410
- var TOOLS = [
1774
+ var TOOLS2 = [
1411
1775
  "npm",
1412
1776
  "pip",
1413
1777
  "pip3",
@@ -1428,7 +1792,7 @@ var TOOLS = [
1428
1792
  register({
1429
1793
  name: "packageManagers",
1430
1794
  async scan(ctx) {
1431
- const data = await detectSimpleAsync(TOOLS, (done, total) => {
1795
+ const data = await detectSimpleAsync(TOOLS2, (done, total) => {
1432
1796
  ctx.progress?.sub(`packageManagers · ${done}/${total}`);
1433
1797
  });
1434
1798
  return { data, checks: [] };
@@ -1447,7 +1811,7 @@ register({
1447
1811
  });
1448
1812
 
1449
1813
  // src/detector/vcpkg.ts
1450
- import { join as join3 } from "node:path";
1814
+ import { join as join4 } from "node:path";
1451
1815
  var CANDIDATES = [
1452
1816
  "C:\\vcpkg\\vcpkg.exe",
1453
1817
  "C:\\src\\vcpkg\\vcpkg.exe",
@@ -1474,7 +1838,7 @@ async function findVcpkg() {
1474
1838
  if (fileExists(c))
1475
1839
  add(c, false);
1476
1840
  if (process.env.VCPKG_ROOT) {
1477
- const p = join3(process.env.VCPKG_ROOT, IS_WIN ? "vcpkg.exe" : "vcpkg");
1841
+ const p = join4(process.env.VCPKG_ROOT, IS_WIN ? "vcpkg.exe" : "vcpkg");
1478
1842
  if (fileExists(p))
1479
1843
  add(p, false);
1480
1844
  }
@@ -1630,11 +1994,11 @@ register({
1630
1994
  });
1631
1995
 
1632
1996
  // src/detector/condaenv.ts
1633
- import { join as join4 } from "node:path";
1634
- import { existsSync as existsSync3 } from "node:fs";
1997
+ import { join as join5 } from "node:path";
1998
+ import { existsSync as existsSync4 } from "node:fs";
1635
1999
  function pythonOf(envPath) {
1636
- const py = IS_WIN ? join4(envPath, "python.exe") : join4(envPath, "bin", "python");
1637
- return existsSync3(py) ? py : null;
2000
+ const py = IS_WIN ? join5(envPath, "python.exe") : join5(envPath, "bin", "python");
2001
+ return existsSync4(py) ? py : null;
1638
2002
  }
1639
2003
  register({
1640
2004
  name: "condaenv",
@@ -1814,8 +2178,8 @@ register({
1814
2178
  });
1815
2179
 
1816
2180
  // src/detector/vsshells.ts
1817
- import { readdirSync as readdirSync3, existsSync as existsSync4, statSync as statSync2 } from "node:fs";
1818
- import { join as join5 } from "node:path";
2181
+ import { readdirSync as readdirSync4, existsSync as existsSync5, statSync as statSync3 } from "node:fs";
2182
+ import { join as join6 } from "node:path";
1819
2183
  var VS_BASES = [
1820
2184
  "C:\\Program Files\\Microsoft Visual Studio",
1821
2185
  "C:\\Program Files (x86)\\Microsoft Visual Studio"
@@ -1828,20 +2192,20 @@ var EDITIONS = ["Community", "Professional", "Enterprise", "BuildTools"];
1828
2192
  function vsRoots() {
1829
2193
  const roots = [];
1830
2194
  for (const b of VS_BASES) {
1831
- if (!existsSync4(b))
2195
+ if (!existsSync5(b))
1832
2196
  continue;
1833
- for (const v of readdirSync3(b)) {
1834
- const verDir = join5(b, v);
1835
- if (!existsSync4(verDir) || !statSync2(verDir).isDirectory())
2197
+ for (const v of readdirSync4(b)) {
2198
+ const verDir = join6(b, v);
2199
+ if (!existsSync5(verDir) || !statSync3(verDir).isDirectory())
1836
2200
  continue;
1837
2201
  const hasEdition = EDITIONS.some((e) => {
1838
- const ed = join5(verDir, e);
1839
- return existsSync4(ed) && statSync2(ed).isDirectory();
2202
+ const ed = join6(verDir, e);
2203
+ return existsSync5(ed) && statSync3(ed).isDirectory();
1840
2204
  });
1841
2205
  if (hasEdition) {
1842
2206
  for (const e of EDITIONS) {
1843
- const ed = join5(verDir, e);
1844
- if (existsSync4(ed) && statSync2(ed).isDirectory())
2207
+ const ed = join6(verDir, e);
2208
+ if (existsSync5(ed) && statSync3(ed).isDirectory())
1845
2209
  roots.push(ed);
1846
2210
  }
1847
2211
  } else {
@@ -1854,37 +2218,37 @@ function vsRoots() {
1854
2218
  function msvcVersionsDesc(msvc) {
1855
2219
  let vers = [];
1856
2220
  try {
1857
- vers = readdirSync3(msvc);
2221
+ vers = readdirSync4(msvc);
1858
2222
  } catch {
1859
2223
  return [];
1860
2224
  }
1861
2225
  return vers.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
1862
2226
  }
1863
2227
  function vsCl(root, arch) {
1864
- const msvc = join5(root, "VC", "Tools", "MSVC");
1865
- if (!existsSync4(msvc))
2228
+ const msvc = join6(root, "VC", "Tools", "MSVC");
2229
+ if (!existsSync5(msvc))
1866
2230
  return null;
1867
2231
  for (const v of msvcVersionsDesc(msvc)) {
1868
- const p = join5(msvc, v, "bin", `Host${arch}`, arch, "cl.exe");
1869
- if (existsSync4(p))
2232
+ const p = join6(msvc, v, "bin", `Host${arch}`, arch, "cl.exe");
2233
+ if (existsSync5(p))
1870
2234
  return p;
1871
2235
  }
1872
2236
  return null;
1873
2237
  }
1874
2238
  function vsMsbuild(root) {
1875
- const base = join5(root, "MSBuild");
1876
- if (!existsSync4(base))
2239
+ const base = join6(root, "MSBuild");
2240
+ if (!existsSync5(base))
1877
2241
  return null;
1878
- const cands = [join5(base, "Current", "Bin", "MSBuild.exe")];
2242
+ const cands = [join6(base, "Current", "Bin", "MSBuild.exe")];
1879
2243
  try {
1880
- for (const v of readdirSync3(base))
1881
- cands.push(join5(base, v, "Bin", "MSBuild.exe"));
2244
+ for (const v of readdirSync4(base))
2245
+ cands.push(join6(base, v, "Bin", "MSBuild.exe"));
1882
2246
  } catch {}
1883
- return cands.find((c) => existsSync4(c)) || null;
2247
+ return cands.find((c) => existsSync5(c)) || null;
1884
2248
  }
1885
2249
  function vsCMake(root) {
1886
- const p = join5(root, "Common7", "IDE", "CommonExtensions", "Microsoft", "CMake", "CMake", "bin", "cmake.exe");
1887
- return existsSync4(p) ? p : null;
2250
+ const p = join6(root, "Common7", "IDE", "CommonExtensions", "Microsoft", "CMake", "CMake", "bin", "cmake.exe");
2251
+ return existsSync5(p) ? p : null;
1888
2252
  }
1889
2253
  function vsDotnet() {
1890
2254
  return whichInDirs("dotnet", pathDirs())[0] || null;
@@ -1907,19 +2271,19 @@ function scanToolchain() {
1907
2271
  }
1908
2272
  function shortcutNames() {
1909
2273
  const names = [];
1910
- const stack = [...START_MENU.filter((d) => existsSync4(d))];
2274
+ const stack = [...START_MENU.filter((d) => existsSync5(d))];
1911
2275
  while (stack.length) {
1912
2276
  const dir = stack.pop();
1913
2277
  let entries = [];
1914
2278
  try {
1915
- entries = readdirSync3(dir);
2279
+ entries = readdirSync4(dir);
1916
2280
  } catch {
1917
2281
  continue;
1918
2282
  }
1919
2283
  for (const e of entries) {
1920
- const full = join5(dir, e);
2284
+ const full = join6(dir, e);
1921
2285
  try {
1922
- if (statSync2(full).isDirectory())
2286
+ if (statSync3(full).isDirectory())
1923
2287
  stack.push(full);
1924
2288
  else if (e.toLowerCase().endsWith(".lnk") && /developer|native tools|vs 20/i.test(e))
1925
2289
  names.push(e.replace(/\.lnk$/i, ""));
@@ -1937,8 +2301,8 @@ function inferKindArch(name) {
1937
2301
  function entryRef(kind) {
1938
2302
  const fn = kind === "powershell" ? "Launch-VsDevShell.ps1" : "VsDevCmd.bat";
1939
2303
  for (const r of vsRoots()) {
1940
- const p = join5(r, "Common7", "Tools", fn);
1941
- if (existsSync4(p))
2304
+ const p = join6(r, "Common7", "Tools", fn);
2305
+ if (existsSync5(p))
1942
2306
  return p;
1943
2307
  }
1944
2308
  return null;
@@ -1980,16 +2344,16 @@ register({
1980
2344
  });
1981
2345
 
1982
2346
  // src/detector/projects.ts
1983
- import { readdirSync as readdirSync4, existsSync as existsSync5, statSync as statSync3 } from "node:fs";
1984
- import { join as join6, basename as basename3, dirname as dirname2 } from "node:path";
2347
+ import { readdirSync as readdirSync5, existsSync as existsSync6, statSync as statSync4 } from "node:fs";
2348
+ import { join as join7, basename as basename3, dirname as dirname2 } from "node:path";
1985
2349
  function isVenv(dir) {
1986
- if (!existsSync5(join6(dir, "pyvenv.cfg")))
2350
+ if (!existsSync6(join7(dir, "pyvenv.cfg")))
1987
2351
  return false;
1988
2352
  const child = IS_WIN_PYVENV(dir);
1989
2353
  return child;
1990
2354
  }
1991
2355
  function IS_WIN_PYVENV(dir) {
1992
- return existsSync5(join6(dir, "Scripts", "activate")) || existsSync5(join6(dir, "Scripts", "activate.bat")) || existsSync5(join6(dir, "bin", "activate"));
2356
+ return existsSync6(join7(dir, "Scripts", "activate")) || existsSync6(join7(dir, "Scripts", "activate.bat")) || existsSync6(join7(dir, "bin", "activate"));
1993
2357
  }
1994
2358
  function walk(root, maxDepth, out, seen) {
1995
2359
  const stack = [[root, 0]];
@@ -1997,14 +2361,14 @@ function walk(root, maxDepth, out, seen) {
1997
2361
  const [dir, depth] = stack.pop();
1998
2362
  let entries = [];
1999
2363
  try {
2000
- entries = readdirSync4(dir);
2364
+ entries = readdirSync5(dir);
2001
2365
  } catch {
2002
2366
  continue;
2003
2367
  }
2004
2368
  for (const e of entries) {
2005
2369
  if (e === "node_modules" || e === ".git" || e === "target" || e === "dist" || e === ".next" || e === "out") {
2006
2370
  if (e === "node_modules") {
2007
- const p = join6(dir, e);
2371
+ const p = join7(dir, e);
2008
2372
  if (!seen.has(p.toLowerCase())) {
2009
2373
  seen.add(p.toLowerCase());
2010
2374
  out.push({ project: basename3(dir), type: "node_modules", path: p });
@@ -2012,10 +2376,10 @@ function walk(root, maxDepth, out, seen) {
2012
2376
  }
2013
2377
  continue;
2014
2378
  }
2015
- const full = join6(dir, e);
2379
+ const full = join7(dir, e);
2016
2380
  let st;
2017
2381
  try {
2018
- st = statSync3(full);
2382
+ st = statSync4(full);
2019
2383
  } catch {
2020
2384
  continue;
2021
2385
  }
@@ -2040,7 +2404,7 @@ register({
2040
2404
  const envs = [];
2041
2405
  const seen = new Set;
2042
2406
  for (const r of roots) {
2043
- if (existsSync5(r))
2407
+ if (existsSync6(r))
2044
2408
  walk(r, 4, envs, seen);
2045
2409
  }
2046
2410
  const checks = [];
@@ -2054,9 +2418,9 @@ register({
2054
2418
  });
2055
2419
 
2056
2420
  // src/cli.ts
2057
- import { writeFileSync, mkdirSync, readFileSync, existsSync as existsSync6 } from "node:fs";
2058
- import { homedir as homedir2 } from "node:os";
2059
- import { join as join7 } from "node:path";
2421
+ import { writeFileSync, mkdirSync, readFileSync as readFileSync2, existsSync as existsSync7 } from "node:fs";
2422
+ import { homedir as homedir3 } from "node:os";
2423
+ import { join as join8 } from "node:path";
2060
2424
  import { execSync } from "node:child_process";
2061
2425
  if (process.platform === "win32") {
2062
2426
  try {
@@ -2069,8 +2433,8 @@ if (process.platform === "win32") {
2069
2433
  const oerr = console.error.bind(console);
2070
2434
  console.error = (...a) => oerr(...a.map((x) => typeof x === "string" ? clean(x) : x));
2071
2435
  }
2072
- var ALL_SHELLS = ["current", "powershell", "gitbash", "wsl"];
2073
- var DEFAULT_MANIFEST = join7(homedir2(), ".devmap", "devmap.json");
2436
+ var ALL_SHELLS2 = ["current", "powershell", "gitbash", "wsl"];
2437
+ var DEFAULT_MANIFEST = join8(homedir3(), ".devmap", "devmap.json");
2074
2438
  function emitText(text, withBom = false) {
2075
2439
  const prefix = withBom && !process.stdout.isTTY ? "\uFEFF" : "";
2076
2440
  process.stdout.write(prefix + text + `
@@ -2105,7 +2469,7 @@ function toShells(v) {
2105
2469
  if (typeof v !== "string")
2106
2470
  return ["current"];
2107
2471
  const list = v.split(",").map((s) => s.trim());
2108
- return list.includes("all") ? ALL_SHELLS : list.length ? list : ["current"];
2472
+ return list.includes("all") ? ALL_SHELLS2 : list.length ? list : ["current"];
2109
2473
  }
2110
2474
  function toSet(v) {
2111
2475
  if (typeof v !== "string")
@@ -2119,10 +2483,10 @@ function toRoots(v) {
2119
2483
  return arr.length ? arr : undefined;
2120
2484
  }
2121
2485
  function readManifestFile(path) {
2122
- if (!existsSync6(path))
2486
+ if (!existsSync7(path))
2123
2487
  throw new Error(`manifest file not found: ${path}`);
2124
2488
  try {
2125
- const raw = readFileSync(path, "utf8").replace(/^\uFEFF/, "");
2489
+ const raw = readFileSync2(path, "utf8").replace(/^\uFEFF/, "");
2126
2490
  return JSON.parse(raw);
2127
2491
  } catch (e) {
2128
2492
  throw new Error(`manifest parse failed (${path}): ${e.message}`);
@@ -2131,7 +2495,7 @@ function readManifestFile(path) {
2131
2495
  async function resolveManifest(explicitPath, shells, disabled, label, roots) {
2132
2496
  if (explicitPath)
2133
2497
  return { manifest: readManifestFile(explicitPath), source: explicitPath };
2134
- if (existsSync6(DEFAULT_MANIFEST))
2498
+ if (existsSync7(DEFAULT_MANIFEST))
2135
2499
  return { manifest: readManifestFile(DEFAULT_MANIFEST), source: DEFAULT_MANIFEST };
2136
2500
  const manifest = await runScan(shells, disabled, { projectRoots: roots });
2137
2501
  return { manifest, source: label + "(live scan)" };
@@ -2146,7 +2510,8 @@ async function cmdScan(opts, pos) {
2146
2510
  checkOutdated: opts.outdated === true,
2147
2511
  wslDeep: opts["wsl-deep"] === true,
2148
2512
  auditDeps: opts["audit-deps"] === true,
2149
- healthNetwork: opts["health-network"] === true
2513
+ healthNetwork: opts["health-network"] === true,
2514
+ scanSecrets: opts["scan-secrets"] === true
2150
2515
  });
2151
2516
  if (opts.json) {
2152
2517
  emitText(JSON.stringify(manifest, null, 2), opts.bom === true);
@@ -2156,7 +2521,7 @@ async function cmdScan(opts, pos) {
2156
2521
  renderPrettyFromManifest(manifest);
2157
2522
  if (opts["no-manifest"] !== true) {
2158
2523
  try {
2159
- mkdirSync(join7(DEFAULT_MANIFEST, ".."), { recursive: true });
2524
+ mkdirSync(join8(DEFAULT_MANIFEST, ".."), { recursive: true });
2160
2525
  writeFileSync(DEFAULT_MANIFEST, JSON.stringify(manifest, null, 2));
2161
2526
  console.log(` ${"\x1B[2m"}manifest → ${DEFAULT_MANIFEST}${"\x1B[0m"}`);
2162
2527
  } catch {}
@@ -2248,7 +2613,7 @@ async function cmdDevdiff(opts, pos) {
2248
2613
  const before = await resolveManifest(opts.manifest, shells, disabled, "devdiff-before", toRoots(opts["scan-root"]));
2249
2614
  A = before.manifest;
2250
2615
  labelA = before.source;
2251
- B = await runScan(shells, disabled, { projectRoots: toRoots(opts["scan-root"]) });
2616
+ B = await runScan(shells, disabled, { projectRoots: toRoots(opts["scan-root"]), scanSecrets: opts["scan-secrets"] === true });
2252
2617
  labelB = "current live scan";
2253
2618
  }
2254
2619
  const diff = diffManifests(A, B);
@@ -2515,9 +2880,11 @@ scan options:
2515
2880
  --wsl-deep probe inside stopped WSL distros (default probes only running ones to avoid waking distros)
2516
2881
  --ci CI gate: clean → exit 0; any warn → exit 2; any error → exit 1
2517
2882
  --audit-deps dependency CVE audit via OSV-Scanner/pip-audit/govulncheck/Trivy (opt-in, uses network)
2883
+ --scan-secrets scan a curated allowlist of HOME/project credential files for hardcoded secrets (opt-in, offline)
2518
2884
  --health-network run network health probes too (npm doctor / rustup check; off by default to stay offline-fast)
2519
2885
  --quiet suppress scan progress feedback (for scripts/CI)
2520
2886
  --bom prepend UTF-8 BOM for viewing in Notepad etc. (only on redirect/pipe)
2887
+ --mcp run as an MCP server (stdio JSON-RPC) for AI IDE agents (Cursor/Cline/Claude Desktop)
2521
2888
  --version / --help
2522
2889
 
2523
2890
  Phase 2 environment layer (covered by this scan):
@@ -2555,6 +2922,10 @@ async function main() {
2555
2922
  console.log(VERSION);
2556
2923
  process.exit(0);
2557
2924
  }
2925
+ if (opts.mcp) {
2926
+ await startMcpServer();
2927
+ process.exit(0);
2928
+ }
2558
2929
  const sub = pos[0] || "scan";
2559
2930
  let code = 0;
2560
2931
  try {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devmap
3
- Version: 0.3.8
3
+ Version: 0.3.9
4
4
  Summary: Map your machine's dev environment and surface issues (PATH conflicts, ghost binaries, multi-version collisions). fastfetch-for-dev-tools. This PyPI package ships the real devmap CLI (a bundled JavaScript app launched via Node.js or Bun), so `pip install devmap && devmap` works standalone.
5
5
  Author: devmap contributors
6
6
  License: MIT
@@ -0,0 +1,7 @@
1
+ devmap/__init__.py,sha256=4Ek7Di0FYYl76FKnHJ8VYiJPHlUTfoqyBB_hul_pFbk,1728
2
+ devmap/_devmap.mjs,sha256=z0vAvLmK_1dVXZkrcMGp7KSYoJTI15sTWRjQwPIANVQ,107512
3
+ devmap-0.3.9.dist-info/METADATA,sha256=UihXpOtSC58_JLOZPRmsnNdPmPC5UeYd4sfPU7Ba9Sc,6977
4
+ devmap-0.3.9.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
5
+ devmap-0.3.9.dist-info/entry_points.txt,sha256=FOQKZCLz0WGWemrTw0suYRqKWrUB4_XJ2Gkrw60ZEcg,39
6
+ devmap-0.3.9.dist-info/top_level.txt,sha256=QeomiD_JKcjSd_jTZv0WJF3darUb2sUsxmocjgYJ6Qc,7
7
+ devmap-0.3.9.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- devmap/__init__.py,sha256=4Ek7Di0FYYl76FKnHJ8VYiJPHlUTfoqyBB_hul_pFbk,1728
2
- devmap/_devmap.mjs,sha256=yRSmxGm1Kx6viFjAPiVNoEWvRJnSSXDZ5RQEie9XFYk,93233
3
- devmap-0.3.8.dist-info/METADATA,sha256=MmUONHZSjGyIvbkWfsELqRr8ZcJQczH88F0Y5HumEtg,6977
4
- devmap-0.3.8.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
5
- devmap-0.3.8.dist-info/entry_points.txt,sha256=FOQKZCLz0WGWemrTw0suYRqKWrUB4_XJ2Gkrw60ZEcg,39
6
- devmap-0.3.8.dist-info/top_level.txt,sha256=QeomiD_JKcjSd_jTZv0WJF3darUb2sUsxmocjgYJ6Qc,7
7
- devmap-0.3.8.dist-info/RECORD,,
File without changes