devmap 0.3.6__tar.gz → 0.3.7__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devmap
3
- Version: 0.3.6
3
+ Version: 0.3.7
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
@@ -40,7 +40,7 @@ Requirements at runtime: a Node.js (or Bun) runtime available on `PATH`. Python
40
40
  - **Off-PATH awareness**: reports installed-but-not-on-PATH tools, while correctly treating version-manager environments (conda envs, nvm versions) as by-design off-PATH.
41
41
  - **Environment layers**: vcpkg packages, per-python pip packages (incl. upgradable), apt packages (Linux), conda environments, Docker (binary + daemon + images/containers), WSL distros, Visual Studio dev shells, and project-local environments (`.venv`, `node_modules`).
42
42
  - **Dependency auditing (optional)**: with `--audit-deps`, devmap detects and invokes installed SCA tools (osv-scanner, govulncheck, pip-audit, Trivy), parses their JSON, and merges results into a unified view. devmap is a *scanner/orchestrator*, not a CVE database — for authoritative vulnerability data, use those tools directly.
43
- - **Health layer**: converges official self-checks into one view — `pip check` (broken dependency requirements, **offline by default**) and, with `--health-network`, `npm doctor` / `rustup check`. Unavailable or offline probes degrade gracefully to `info`.
43
+ - **Health layer**: converges official self-checks into one view, **per environment**. Offline by default: `pip check` runs for **every** Python interpreter, and `conda doctor` runs for **every** conda env. With `--health-network`, `npm doctor` / `rustup check` are added. Missing/offline probes degrade gracefully to `info` (skipped). Only `warn`/`error` surface under `issues (N)`; passing checks are summarized as `✓ N checks passed`.
44
44
  - **Three sub-commands**: `scan` (the fact source), `query` (ask the manifest), `devdiff` (compare two manifests — environment drift / cross-machine consistency).
45
45
  - **Zero Python dependencies.** The bundled app is pure TypeScript compiled to JavaScript.
46
46
 
@@ -22,7 +22,7 @@ Requirements at runtime: a Node.js (or Bun) runtime available on `PATH`. Python
22
22
  - **Off-PATH awareness**: reports installed-but-not-on-PATH tools, while correctly treating version-manager environments (conda envs, nvm versions) as by-design off-PATH.
23
23
  - **Environment layers**: vcpkg packages, per-python pip packages (incl. upgradable), apt packages (Linux), conda environments, Docker (binary + daemon + images/containers), WSL distros, Visual Studio dev shells, and project-local environments (`.venv`, `node_modules`).
24
24
  - **Dependency auditing (optional)**: with `--audit-deps`, devmap detects and invokes installed SCA tools (osv-scanner, govulncheck, pip-audit, Trivy), parses their JSON, and merges results into a unified view. devmap is a *scanner/orchestrator*, not a CVE database — for authoritative vulnerability data, use those tools directly.
25
- - **Health layer**: converges official self-checks into one view — `pip check` (broken dependency requirements, **offline by default**) and, with `--health-network`, `npm doctor` / `rustup check`. Unavailable or offline probes degrade gracefully to `info`.
25
+ - **Health layer**: converges official self-checks into one view, **per environment**. Offline by default: `pip check` runs for **every** Python interpreter, and `conda doctor` runs for **every** conda env. With `--health-network`, `npm doctor` / `rustup check` are added. Missing/offline probes degrade gracefully to `info` (skipped). Only `warn`/`error` surface under `issues (N)`; passing checks are summarized as `✓ N checks passed`.
26
26
  - **Three sub-commands**: `scan` (the fact source), `query` (ask the manifest), `devdiff` (compare two manifests — environment drift / cross-machine consistency).
27
27
  - **Zero Python dependencies.** The bundled app is pure TypeScript compiled to JavaScript.
28
28
 
@@ -2,7 +2,7 @@
2
2
  // @bun
3
3
 
4
4
  // src/version.ts
5
- var VERSION = "0.3.6";
5
+ var VERSION = "0.3.7";
6
6
 
7
7
  // src/install.ts
8
8
  function isIsolated(i) {
@@ -719,23 +719,46 @@ function parseRustupCheck(text) {
719
719
  return { status: "ok", summary: "rustup toolchains up to date" };
720
720
  return { status: "ok", summary: "rustup check passed" };
721
721
  }
722
+ function parseCondaDoctor(text) {
723
+ const t = (text || "").trim();
724
+ if (!t)
725
+ return { status: "skipped", summary: "conda doctor produced no output (timed out or silent)" };
726
+ const lines = t.split(/\r?\n/).map((l) => l.trim());
727
+ const problemSections = [];
728
+ let cur = null;
729
+ for (const line of lines) {
730
+ if (!line)
731
+ continue;
732
+ const hdr = line.match(/^[❌✅]\s*(.+?)\s*:?\s*$/);
733
+ if (hdr) {
734
+ const name = hdr[1].replace(/:\s*$/, "").trim();
735
+ cur = { name, items: 0 };
736
+ if (line.startsWith("❌"))
737
+ problemSections.push(cur);
738
+ continue;
739
+ }
740
+ if (cur) {
741
+ const m = line.match(/:\s*(\d+)\s*$/);
742
+ if (m)
743
+ cur.items += parseInt(m[1], 10);
744
+ }
745
+ }
746
+ const real = problemSections.filter((p) => p.items > 0);
747
+ if (real.length === 0) {
748
+ return { status: "ok", summary: "conda environment healthy", detail: t };
749
+ }
750
+ const total = real.reduce((a, p) => a + p.items, 0);
751
+ const detail = real.map((p) => `${p.name}: ${p.items}`).join(`
752
+ `);
753
+ return {
754
+ status: "warn",
755
+ summary: `conda doctor: ${real.length} check${real.length === 1 ? "" : "s"} failed (${total} affected files)`,
756
+ detail
757
+ };
758
+ }
722
759
 
723
760
  // src/health/scan.ts
724
- var PROBES = [
725
- {
726
- id: "pip",
727
- label: "pip",
728
- kind: "conflict",
729
- network: false,
730
- candidates: [
731
- ["pip", ["check"]],
732
- ["pip3", ["check"]],
733
- ["python", ["-m", "pip", "check"]],
734
- ["python3", ["-m", "pip", "check"]]
735
- ],
736
- timeout: 15000,
737
- parse: parsePipCheck
738
- },
761
+ var NETWORK_PROBES = [
739
762
  {
740
763
  id: "npm",
741
764
  label: "npm",
@@ -755,6 +778,23 @@ var PROBES = [
755
778
  parse: parseRustupCheck
756
779
  }
757
780
  ];
781
+ var SEV_OF = {
782
+ ok: "ok",
783
+ warn: "warn",
784
+ info: "info",
785
+ skipped: "info"
786
+ };
787
+ function skippedCheck(prefix, kind, summary) {
788
+ return makeCheck(`health.${prefix}.skipped`, kind, "info", summary, "");
789
+ }
790
+ function toCheckPerEnv(prefix, envLabel, r, kind, path) {
791
+ if (r.status === "skipped") {
792
+ return makeCheck(`health.${prefix}.${envLabel}.skipped`, kind, "info", `${prefix} doctor (${envLabel}) produced no output`, "");
793
+ }
794
+ const id = `health.${prefix}.${envLabel}.check`;
795
+ const summary = r.status === "ok" ? `${prefix} check (${envLabel}): consistent` : r.summary;
796
+ return makeCheck(id, kind, SEV_OF[r.status], summary, r.detail || "", path ? [path] : []);
797
+ }
758
798
  function pickCandidate(p) {
759
799
  const dirs = pathDirs();
760
800
  for (const [cmd, args] of p.candidates) {
@@ -763,33 +803,49 @@ function pickCandidate(p) {
763
803
  }
764
804
  return null;
765
805
  }
766
- var SEV_OF = {
767
- ok: "ok",
768
- warn: "warn",
769
- info: "info",
770
- skipped: "info"
771
- };
772
- function toCheck(p, r) {
806
+ function toCheckNetwork(p, r) {
773
807
  const sev = SEV_OF[r.status];
774
- const id = `health.${p.id}.check`;
775
- const realId = r.status === "skipped" ? `health.${p.id}.skipped` : id;
776
- return makeCheck(realId, p.kind, sev, r.summary, r.detail || "", [], r.status === "ok" ? undefined : undefined);
808
+ const id = r.status === "skipped" ? `health.${p.id}.skipped` : `health.${p.id}.check`;
809
+ return makeCheck(id, p.kind, sev, r.summary, r.detail || "");
777
810
  }
778
811
  async function computeHealth(opts = {}) {
779
812
  const checks = [];
780
813
  const network = opts.healthNetwork === true;
781
- for (const p of PROBES) {
782
- const picked = pickCandidate(p);
783
- if (!picked)
784
- continue;
814
+ const pythons = opts.pythons || [];
815
+ if (pythons.length === 0) {
816
+ checks.push(skippedCheck("pip", "conflict", "no python interpreters detected (pip check skipped)"));
817
+ } else {
818
+ await Promise.all(pythons.map(async (py) => {
819
+ const out = await runBinAsync(py.path, ["-m", "pip", "check"], 15000);
820
+ const r = parsePipCheck(out);
821
+ checks.push(toCheckPerEnv("pip", py.label, r, "conflict", py.path));
822
+ }));
823
+ }
824
+ const condaEnvs = opts.condaEnvs || [];
825
+ const condaOnPath = whichInDirs("conda", pathDirs()).length > 0;
826
+ if (!condaOnPath) {
827
+ checks.push(skippedCheck("conda", "stale", "conda not on PATH (conda doctor skipped)"));
828
+ } else if (condaEnvs.length === 0) {
829
+ checks.push(skippedCheck("conda", "stale", "no conda environments detected (conda doctor skipped)"));
830
+ } else {
831
+ await Promise.all(condaEnvs.map(async (e) => {
832
+ const out = await runAsync("conda", ["doctor", "-n", e.name], 30000);
833
+ const r = parseCondaDoctor(out);
834
+ checks.push(toCheckPerEnv("conda", e.name, r, "stale", e.pythonPath || undefined));
835
+ }));
836
+ }
837
+ for (const p of NETWORK_PROBES) {
785
838
  if (p.network && !network) {
786
839
  checks.push(makeCheck(`health.${p.id}.skipped`, p.kind, "info", `${p.label} doctor skipped (offline by default)`, `Run with --health-network to invoke ${p.label} self-checks (they require network).`));
787
840
  continue;
788
841
  }
842
+ const picked = pickCandidate(p);
843
+ if (!picked)
844
+ continue;
789
845
  const [cmd, args] = picked;
790
846
  const out = await runAsync(cmd, args, p.timeout);
791
847
  const result = p.parse(out);
792
- checks.push(toCheck(p, result));
848
+ checks.push(toCheckNetwork(p, result));
793
849
  }
794
850
  return checks;
795
851
  }
@@ -964,6 +1020,46 @@ class ScanProgress {
964
1020
  }
965
1021
 
966
1022
  // src/scan.ts
1023
+ import { realpathSync } from "node:fs";
1024
+ import { dirname } from "node:path";
1025
+ function healthPythonLabel(path) {
1026
+ const lower = path.toLowerCase().replace(/\\/g, "/");
1027
+ const env = lower.match(/[\\/]envs[\\/]([^\\/]+)[\\/]/);
1028
+ if (env)
1029
+ return env[1];
1030
+ if (/miniconda3[\\/]python/i.test(lower) || /anaconda3[\\/]python/i.test(lower))
1031
+ return "base";
1032
+ if (lower.includes("workbuddy/binaries/python"))
1033
+ return "managed";
1034
+ if (lower.includes("/nvm") || lower.includes("\\nvm"))
1035
+ return "nvm";
1036
+ const parts = lower.split("/").filter(Boolean);
1037
+ const parent = parts[parts.length - 2] || "";
1038
+ return parent || "python";
1039
+ }
1040
+ function dedupePythons(raw) {
1041
+ const byDir = new Map;
1042
+ for (const p of raw) {
1043
+ let rp = p.pythonPath;
1044
+ try {
1045
+ rp = realpathSync(p.pythonPath);
1046
+ } catch {}
1047
+ const dir = dirname(rp);
1048
+ if (!byDir.has(dir))
1049
+ byDir.set(dir, p.pythonPath);
1050
+ }
1051
+ const out = [];
1052
+ const used = new Set;
1053
+ for (const path of byDir.values()) {
1054
+ let label = healthPythonLabel(path);
1055
+ let n = 2;
1056
+ while (used.has(label))
1057
+ label = `${healthPythonLabel(path)}#${n++}`;
1058
+ used.add(label);
1059
+ out.push({ path, label });
1060
+ }
1061
+ return out;
1062
+ }
967
1063
  async function runScan(shells, disabled, opts = {}) {
968
1064
  const progress = opts.silent ? undefined : new ScanProgress(0, process.stderr.isTTY);
969
1065
  const ctx = {
@@ -983,7 +1079,12 @@ async function runScan(shells, disabled, opts = {}) {
983
1079
  ...byName.ides || []
984
1080
  ];
985
1081
  const conflicts = computeConflicts(groups, shells, disabled);
986
- const healthChecks = await computeHealth({ healthNetwork: opts.healthNetwork });
1082
+ const pythons = dedupePythons(byName.pip?.perInterpreter || []);
1083
+ const condaEnvs = (byName.condaenv?.envs || []).map((e) => ({
1084
+ name: e.name,
1085
+ pythonPath: e.pythonPath
1086
+ }));
1087
+ const healthChecks = await computeHealth({ healthNetwork: opts.healthNetwork, pythons, condaEnvs });
987
1088
  const securityChecks = [
988
1089
  ...computeSecurity(byName),
989
1090
  ...computeScaInventory(),
@@ -1876,7 +1977,7 @@ register({
1876
1977
 
1877
1978
  // src/detector/projects.ts
1878
1979
  import { readdirSync as readdirSync4, existsSync as existsSync5, statSync as statSync3 } from "node:fs";
1879
- import { join as join6, basename as basename3, dirname } from "node:path";
1980
+ import { join as join6, basename as basename3, dirname as dirname2 } from "node:path";
1880
1981
  function isVenv(dir) {
1881
1982
  if (!existsSync5(join6(dir, "pyvenv.cfg")))
1882
1983
  return false;
@@ -1919,7 +2020,7 @@ function walk(root, maxDepth, out, seen) {
1919
2020
  if (e === ".venv" || e === "venv") {
1920
2021
  if (isVenv(full) && !seen.has(full.toLowerCase())) {
1921
2022
  seen.add(full.toLowerCase());
1922
- out.push({ project: basename3(dirname(full)), type: "venv", path: full });
2023
+ out.push({ project: basename3(dirname2(full)), type: "venv", path: full });
1923
2024
  }
1924
2025
  continue;
1925
2026
  }
@@ -2098,13 +2199,25 @@ async function cmdQuery(opts, pos) {
2098
2199
  const c = result.conflicts;
2099
2200
  console.log(` \x1B[1mconflicts\x1B[0m multiVersion=${c.multiVersionTools?.length || 0} path=${c.pathConflicts?.length || 0} ghost=${c.ghostBinaries?.length || 0}`);
2100
2201
  }
2101
- if (result.checks && result.checks.length) {
2102
- console.log(` \x1B[1missues (${result.checks.length})\x1B[0m`);
2103
- for (const c of result.checks) {
2202
+ const allChecks = result.checks || [];
2203
+ const issues = allChecks.filter((c) => bucketOf(c.Severity) === "issue");
2204
+ const infos = allChecks.filter((c) => bucketOf(c.Severity) === "info");
2205
+ const oks = allChecks.filter((c) => bucketOf(c.Severity) === "ok");
2206
+ if (issues.length) {
2207
+ console.log(` \x1B[1missues (${issues.length})\x1B[0m`);
2208
+ for (const c of issues) {
2104
2209
  const sev = sevColor(c.Severity);
2105
2210
  console.log(` ${sev}${c.Severity}\x1B[0m ${c.ID} ${c.Label}`);
2106
2211
  }
2107
2212
  }
2213
+ if (infos.length) {
2214
+ console.log(` \x1B[1minfo (${infos.length})\x1B[0m`);
2215
+ for (const c of infos)
2216
+ console.log(` ${sevColor(c.Severity)}${c.Severity}\x1B[0m ${c.ID} ${c.Label}`);
2217
+ }
2218
+ if (oks.length) {
2219
+ console.log(` \x1B[32m✓ ${oks.length} checks passed\x1B[0m`);
2220
+ }
2108
2221
  if (!result.groups.length && !result.checks.length && !result.conflicts) {
2109
2222
  console.log(" (no match)");
2110
2223
  }
@@ -2174,6 +2287,13 @@ function sevColor(sev) {
2174
2287
  return "\x1B[0m";
2175
2288
  }
2176
2289
  }
2290
+ function bucketOf(sev) {
2291
+ if (sev === "error" || sev === "warn" || sev === "missing")
2292
+ return "issue";
2293
+ if (sev === "info")
2294
+ return "info";
2295
+ return "ok";
2296
+ }
2177
2297
  function renderPrettyFromManifest(m) {
2178
2298
  const sys = m.system || {};
2179
2299
  console.log(`\x1B[1mdevmap\x1B[0m v${m.meta?.version || VERSION} \x1B[2m${sys.hostname || "?"}\x1B[0m ${sys.platform}/${sys.arch}`);
@@ -2188,13 +2308,26 @@ function renderPrettyFromManifest(m) {
2188
2308
  }
2189
2309
  }
2190
2310
  }
2191
- const checks = m.checks || [];
2311
+ const allChecks = m.checks || [];
2312
+ const issues = allChecks.filter((c) => bucketOf(c.Severity) === "issue");
2313
+ const infos = allChecks.filter((c) => bucketOf(c.Severity) === "info");
2314
+ const oks = allChecks.filter((c) => bucketOf(c.Severity) === "ok");
2192
2315
  renderEnvironments(m.environments);
2193
- if (checks.length) {
2316
+ if (issues.length || infos.length || oks.length) {
2194
2317
  console.log(" " + "─".repeat(40));
2195
- console.log(` \x1B[1missues (${checks.length})\x1B[0m`);
2196
- for (const c of checks)
2197
- console.log(` ${sevColor(c.Severity)}${c.Severity}\x1B[0m ${c.ID} ${c.Label}`);
2318
+ if (issues.length) {
2319
+ console.log(` \x1B[1missues (${issues.length})\x1B[0m`);
2320
+ for (const c of issues)
2321
+ console.log(` ${sevColor(c.Severity)}${c.Severity}\x1B[0m ${c.ID} ${c.Label}`);
2322
+ }
2323
+ if (infos.length) {
2324
+ console.log(` \x1B[1minfo (${infos.length})\x1B[0m`);
2325
+ for (const c of infos)
2326
+ console.log(` ${sevColor(c.Severity)}${c.Severity}\x1B[0m ${c.ID} ${c.Label}`);
2327
+ }
2328
+ if (oks.length) {
2329
+ console.log(` \x1B[32m✓ ${oks.length} checks passed\x1B[0m`);
2330
+ }
2198
2331
  }
2199
2332
  }
2200
2333
  var plur = (n, one, many = one + "s") => `${n} ${n === 1 ? one : many}`;
@@ -2276,11 +2409,25 @@ function renderMarkdownLite(m) {
2276
2409
  }
2277
2410
  lines.push("");
2278
2411
  }
2279
- if ((m.checks || []).length) {
2280
- lines.push(`## issues (${m.checks.length})`);
2281
- for (const c of m.checks)
2412
+ const allChecks = m.checks || [];
2413
+ const issues = allChecks.filter((c) => bucketOf(c.Severity) === "issue");
2414
+ const infos = allChecks.filter((c) => bucketOf(c.Severity) === "info");
2415
+ const oks = allChecks.filter((c) => bucketOf(c.Severity) === "ok");
2416
+ if (issues.length) {
2417
+ lines.push(`## issues (${issues.length})`);
2418
+ for (const c of issues)
2282
2419
  lines.push(`- [${c.Severity}] \`${c.ID}\` ${c.Label} — ${c.Detail}`);
2283
2420
  }
2421
+ if (infos.length) {
2422
+ lines.push(`## info (${infos.length})`);
2423
+ for (const c of infos)
2424
+ lines.push(`- [${c.Severity}] \`${c.ID}\` ${c.Label} — ${c.Detail}`);
2425
+ }
2426
+ if (oks.length) {
2427
+ lines.push(`## checks passed (${oks.length})`);
2428
+ for (const c of oks)
2429
+ lines.push(`- [${c.Severity}] \`${c.ID}\` ${c.Label}`);
2430
+ }
2284
2431
  const env = m.environments || {};
2285
2432
  const envLines = [];
2286
2433
  if (env.vcpkg?.installed)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devmap
3
- Version: 0.3.6
3
+ Version: 0.3.7
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
@@ -40,7 +40,7 @@ Requirements at runtime: a Node.js (or Bun) runtime available on `PATH`. Python
40
40
  - **Off-PATH awareness**: reports installed-but-not-on-PATH tools, while correctly treating version-manager environments (conda envs, nvm versions) as by-design off-PATH.
41
41
  - **Environment layers**: vcpkg packages, per-python pip packages (incl. upgradable), apt packages (Linux), conda environments, Docker (binary + daemon + images/containers), WSL distros, Visual Studio dev shells, and project-local environments (`.venv`, `node_modules`).
42
42
  - **Dependency auditing (optional)**: with `--audit-deps`, devmap detects and invokes installed SCA tools (osv-scanner, govulncheck, pip-audit, Trivy), parses their JSON, and merges results into a unified view. devmap is a *scanner/orchestrator*, not a CVE database — for authoritative vulnerability data, use those tools directly.
43
- - **Health layer**: converges official self-checks into one view — `pip check` (broken dependency requirements, **offline by default**) and, with `--health-network`, `npm doctor` / `rustup check`. Unavailable or offline probes degrade gracefully to `info`.
43
+ - **Health layer**: converges official self-checks into one view, **per environment**. Offline by default: `pip check` runs for **every** Python interpreter, and `conda doctor` runs for **every** conda env. With `--health-network`, `npm doctor` / `rustup check` are added. Missing/offline probes degrade gracefully to `info` (skipped). Only `warn`/`error` surface under `issues (N)`; passing checks are summarized as `✓ N checks passed`.
44
44
  - **Three sub-commands**: `scan` (the fact source), `query` (ask the manifest), `devdiff` (compare two manifests — environment drift / cross-machine consistency).
45
45
  - **Zero Python dependencies.** The bundled app is pure TypeScript compiled to JavaScript.
46
46
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devmap"
7
- version = "0.3.6"
7
+ version = "0.3.7"
8
8
  description = "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."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
File without changes
File without changes