devmap 0.3.5__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.
- {devmap-0.3.5 → devmap-0.3.7}/PKG-INFO +2 -1
- {devmap-0.3.5 → devmap-0.3.7}/README.md +1 -0
- {devmap-0.3.5 → devmap-0.3.7}/devmap/_devmap.mjs +300 -17
- {devmap-0.3.5 → devmap-0.3.7}/devmap.egg-info/PKG-INFO +2 -1
- {devmap-0.3.5 → devmap-0.3.7}/pyproject.toml +1 -1
- {devmap-0.3.5 → devmap-0.3.7}/devmap/__init__.py +0 -0
- {devmap-0.3.5 → devmap-0.3.7}/devmap.egg-info/SOURCES.txt +0 -0
- {devmap-0.3.5 → devmap-0.3.7}/devmap.egg-info/dependency_links.txt +0 -0
- {devmap-0.3.5 → devmap-0.3.7}/devmap.egg-info/entry_points.txt +0 -0
- {devmap-0.3.5 → devmap-0.3.7}/devmap.egg-info/top_level.txt +0 -0
- {devmap-0.3.5 → devmap-0.3.7}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devmap
|
|
3
|
-
Version: 0.3.
|
|
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,6 +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, **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`.
|
|
43
44
|
- **Three sub-commands**: `scan` (the fact source), `query` (ask the manifest), `devdiff` (compare two manifests — environment drift / cross-machine consistency).
|
|
44
45
|
- **Zero Python dependencies.** The bundled app is pure TypeScript compiled to JavaScript.
|
|
45
46
|
|
|
@@ -22,6 +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, **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`.
|
|
25
26
|
- **Three sub-commands**: `scan` (the fact source), `query` (ask the manifest), `devdiff` (compare two manifests — environment drift / cross-machine consistency).
|
|
26
27
|
- **Zero Python dependencies.** The bundled app is pure TypeScript compiled to JavaScript.
|
|
27
28
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/version.ts
|
|
5
|
-
var VERSION = "0.3.
|
|
5
|
+
var VERSION = "0.3.7";
|
|
6
6
|
|
|
7
7
|
// src/install.ts
|
|
8
8
|
function isIsolated(i) {
|
|
@@ -662,6 +662,194 @@ function runScaAudit(cwd) {
|
|
|
662
662
|
return checks;
|
|
663
663
|
}
|
|
664
664
|
|
|
665
|
+
// src/health/parse.ts
|
|
666
|
+
function parsePipCheck(text) {
|
|
667
|
+
const lines = (text || "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
668
|
+
const clean = lines.some((l) => /^no broken requirements found/i.test(l));
|
|
669
|
+
const conflicts = lines.filter((l) => !/^no broken requirements found/i.test(l));
|
|
670
|
+
if (clean || conflicts.length === 0) {
|
|
671
|
+
return { status: "ok", summary: "pip dependencies consistent" };
|
|
672
|
+
}
|
|
673
|
+
return {
|
|
674
|
+
status: "warn",
|
|
675
|
+
summary: `pip has ${conflicts.length} broken dependency requirement${conflicts.length === 1 ? "" : "s"}`,
|
|
676
|
+
detail: conflicts.join(`
|
|
677
|
+
`)
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
function parseNpmDoctor(text) {
|
|
681
|
+
const t = (text || "").trim();
|
|
682
|
+
if (!t)
|
|
683
|
+
return { status: "skipped", summary: "npm doctor produced no output (timed out or silent)" };
|
|
684
|
+
const lines = t.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
685
|
+
const NET_RE = /ECONNREFUSED|ETIMEDOUT|ENOTFOUND|getaddrinfo|EAI_|network request|registry\.npmjs\.org|request to https?:/i;
|
|
686
|
+
const BOILER_RE = /complete log of this run|^\s+at\s|node:internal|file:\/\//i;
|
|
687
|
+
const netLines = lines.filter((l) => NET_RE.test(l));
|
|
688
|
+
const failLines = lines.filter((l) => /npm error|✗|✖/i.test(l) && !BOILER_RE.test(l));
|
|
689
|
+
const realFails = failLines.filter((l) => !NET_RE.test(l));
|
|
690
|
+
if (realFails.length) {
|
|
691
|
+
return {
|
|
692
|
+
status: "warn",
|
|
693
|
+
summary: `npm doctor reported ${realFails.length} issue${realFails.length === 1 ? "" : "s"}`,
|
|
694
|
+
detail: realFails.join(`
|
|
695
|
+
`)
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
if (netLines.length) {
|
|
699
|
+
return { status: "skipped", summary: "npm doctor skipped (registry/network unreachable)" };
|
|
700
|
+
}
|
|
701
|
+
return { status: "ok", summary: "npm doctor passed" };
|
|
702
|
+
}
|
|
703
|
+
function parseRustupCheck(text) {
|
|
704
|
+
const t = (text || "").trim();
|
|
705
|
+
if (!t)
|
|
706
|
+
return { status: "skipped", summary: "rustup check produced no output (timed out or silent)" };
|
|
707
|
+
const lines = t.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
708
|
+
const updates = lines.filter((l) => /update available/i.test(l));
|
|
709
|
+
const upToDate = lines.filter((l) => /up to date/i.test(l));
|
|
710
|
+
if (updates.length) {
|
|
711
|
+
return {
|
|
712
|
+
status: "info",
|
|
713
|
+
summary: `rustup: ${updates.length} toolchain update${updates.length === 1 ? "" : "s"} available`,
|
|
714
|
+
detail: updates.join(`
|
|
715
|
+
`)
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
if (upToDate.length)
|
|
719
|
+
return { status: "ok", summary: "rustup toolchains up to date" };
|
|
720
|
+
return { status: "ok", summary: "rustup check passed" };
|
|
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
|
+
}
|
|
759
|
+
|
|
760
|
+
// src/health/scan.ts
|
|
761
|
+
var NETWORK_PROBES = [
|
|
762
|
+
{
|
|
763
|
+
id: "npm",
|
|
764
|
+
label: "npm",
|
|
765
|
+
kind: "mismatch",
|
|
766
|
+
network: true,
|
|
767
|
+
candidates: [["npm", ["doctor"]]],
|
|
768
|
+
timeout: 20000,
|
|
769
|
+
parse: parseNpmDoctor
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
id: "rustup",
|
|
773
|
+
label: "rustup",
|
|
774
|
+
kind: "stale",
|
|
775
|
+
network: true,
|
|
776
|
+
candidates: [["rustup", ["check"]]],
|
|
777
|
+
timeout: 15000,
|
|
778
|
+
parse: parseRustupCheck
|
|
779
|
+
}
|
|
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
|
+
}
|
|
798
|
+
function pickCandidate(p) {
|
|
799
|
+
const dirs = pathDirs();
|
|
800
|
+
for (const [cmd, args] of p.candidates) {
|
|
801
|
+
if (whichInDirs(cmd, dirs).length > 0)
|
|
802
|
+
return [cmd, args];
|
|
803
|
+
}
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
function toCheckNetwork(p, r) {
|
|
807
|
+
const sev = SEV_OF[r.status];
|
|
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 || "");
|
|
810
|
+
}
|
|
811
|
+
async function computeHealth(opts = {}) {
|
|
812
|
+
const checks = [];
|
|
813
|
+
const network = opts.healthNetwork === true;
|
|
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) {
|
|
838
|
+
if (p.network && !network) {
|
|
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).`));
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
const picked = pickCandidate(p);
|
|
843
|
+
if (!picked)
|
|
844
|
+
continue;
|
|
845
|
+
const [cmd, args] = picked;
|
|
846
|
+
const out = await runAsync(cmd, args, p.timeout);
|
|
847
|
+
const result = p.parse(out);
|
|
848
|
+
checks.push(toCheckNetwork(p, result));
|
|
849
|
+
}
|
|
850
|
+
return checks;
|
|
851
|
+
}
|
|
852
|
+
|
|
665
853
|
// src/output/json.ts
|
|
666
854
|
function buildManifest(byName, conflicts, checks, environments, shells) {
|
|
667
855
|
const sys = byName.system || {};
|
|
@@ -832,6 +1020,46 @@ class ScanProgress {
|
|
|
832
1020
|
}
|
|
833
1021
|
|
|
834
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
|
+
}
|
|
835
1063
|
async function runScan(shells, disabled, opts = {}) {
|
|
836
1064
|
const progress = opts.silent ? undefined : new ScanProgress(0, process.stderr.isTTY);
|
|
837
1065
|
const ctx = {
|
|
@@ -841,7 +1069,8 @@ async function runScan(shells, disabled, opts = {}) {
|
|
|
841
1069
|
progress,
|
|
842
1070
|
checkOutdated: opts.checkOutdated,
|
|
843
1071
|
wslDeep: opts.wslDeep,
|
|
844
|
-
auditDeps: opts.auditDeps
|
|
1072
|
+
auditDeps: opts.auditDeps,
|
|
1073
|
+
healthNetwork: opts.healthNetwork
|
|
845
1074
|
};
|
|
846
1075
|
const { byName, allChecks } = await runDetectors(ctx, progress);
|
|
847
1076
|
const groups = [
|
|
@@ -850,12 +1079,18 @@ async function runScan(shells, disabled, opts = {}) {
|
|
|
850
1079
|
...byName.ides || []
|
|
851
1080
|
];
|
|
852
1081
|
const conflicts = computeConflicts(groups, shells, disabled);
|
|
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 });
|
|
853
1088
|
const securityChecks = [
|
|
854
1089
|
...computeSecurity(byName),
|
|
855
1090
|
...computeScaInventory(),
|
|
856
1091
|
...opts.auditDeps ? runScaAudit(process.cwd()) : []
|
|
857
1092
|
];
|
|
858
|
-
const collected = [...allChecks, ...conflicts.checks, ...securityChecks].filter((c) => !isCheckDisabled(c.ID, disabled));
|
|
1093
|
+
const collected = [...allChecks, ...conflicts.checks, ...securityChecks, ...healthChecks].filter((c) => !isCheckDisabled(c.ID, disabled));
|
|
859
1094
|
const environments = {
|
|
860
1095
|
vcpkg: byName.vcpkg,
|
|
861
1096
|
pip: byName.pip,
|
|
@@ -1742,7 +1977,7 @@ register({
|
|
|
1742
1977
|
|
|
1743
1978
|
// src/detector/projects.ts
|
|
1744
1979
|
import { readdirSync as readdirSync4, existsSync as existsSync5, statSync as statSync3 } from "node:fs";
|
|
1745
|
-
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";
|
|
1746
1981
|
function isVenv(dir) {
|
|
1747
1982
|
if (!existsSync5(join6(dir, "pyvenv.cfg")))
|
|
1748
1983
|
return false;
|
|
@@ -1785,7 +2020,7 @@ function walk(root, maxDepth, out, seen) {
|
|
|
1785
2020
|
if (e === ".venv" || e === "venv") {
|
|
1786
2021
|
if (isVenv(full) && !seen.has(full.toLowerCase())) {
|
|
1787
2022
|
seen.add(full.toLowerCase());
|
|
1788
|
-
out.push({ project: basename3(
|
|
2023
|
+
out.push({ project: basename3(dirname2(full)), type: "venv", path: full });
|
|
1789
2024
|
}
|
|
1790
2025
|
continue;
|
|
1791
2026
|
}
|
|
@@ -1906,7 +2141,8 @@ async function cmdScan(opts, pos) {
|
|
|
1906
2141
|
silent: opts.quiet === true,
|
|
1907
2142
|
checkOutdated: opts.outdated === true,
|
|
1908
2143
|
wslDeep: opts["wsl-deep"] === true,
|
|
1909
|
-
auditDeps: opts["audit-deps"] === true
|
|
2144
|
+
auditDeps: opts["audit-deps"] === true,
|
|
2145
|
+
healthNetwork: opts["health-network"] === true
|
|
1910
2146
|
});
|
|
1911
2147
|
if (opts.json) {
|
|
1912
2148
|
emitText(JSON.stringify(manifest, null, 2), opts.bom === true);
|
|
@@ -1963,13 +2199,25 @@ async function cmdQuery(opts, pos) {
|
|
|
1963
2199
|
const c = result.conflicts;
|
|
1964
2200
|
console.log(` \x1B[1mconflicts\x1B[0m multiVersion=${c.multiVersionTools?.length || 0} path=${c.pathConflicts?.length || 0} ghost=${c.ghostBinaries?.length || 0}`);
|
|
1965
2201
|
}
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
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) {
|
|
1969
2209
|
const sev = sevColor(c.Severity);
|
|
1970
2210
|
console.log(` ${sev}${c.Severity}\x1B[0m ${c.ID} ${c.Label}`);
|
|
1971
2211
|
}
|
|
1972
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
|
+
}
|
|
1973
2221
|
if (!result.groups.length && !result.checks.length && !result.conflicts) {
|
|
1974
2222
|
console.log(" (no match)");
|
|
1975
2223
|
}
|
|
@@ -2039,6 +2287,13 @@ function sevColor(sev) {
|
|
|
2039
2287
|
return "\x1B[0m";
|
|
2040
2288
|
}
|
|
2041
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
|
+
}
|
|
2042
2297
|
function renderPrettyFromManifest(m) {
|
|
2043
2298
|
const sys = m.system || {};
|
|
2044
2299
|
console.log(`\x1B[1mdevmap\x1B[0m v${m.meta?.version || VERSION} \x1B[2m${sys.hostname || "?"}\x1B[0m ${sys.platform}/${sys.arch}`);
|
|
@@ -2053,13 +2308,26 @@ function renderPrettyFromManifest(m) {
|
|
|
2053
2308
|
}
|
|
2054
2309
|
}
|
|
2055
2310
|
}
|
|
2056
|
-
const
|
|
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");
|
|
2057
2315
|
renderEnvironments(m.environments);
|
|
2058
|
-
if (
|
|
2316
|
+
if (issues.length || infos.length || oks.length) {
|
|
2059
2317
|
console.log(" " + "─".repeat(40));
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
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
|
+
}
|
|
2063
2331
|
}
|
|
2064
2332
|
}
|
|
2065
2333
|
var plur = (n, one, many = one + "s") => `${n} ${n === 1 ? one : many}`;
|
|
@@ -2141,11 +2409,25 @@ function renderMarkdownLite(m) {
|
|
|
2141
2409
|
}
|
|
2142
2410
|
lines.push("");
|
|
2143
2411
|
}
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
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)
|
|
2419
|
+
lines.push(`- [${c.Severity}] \`${c.ID}\` ${c.Label} — ${c.Detail}`);
|
|
2420
|
+
}
|
|
2421
|
+
if (infos.length) {
|
|
2422
|
+
lines.push(`## info (${infos.length})`);
|
|
2423
|
+
for (const c of infos)
|
|
2147
2424
|
lines.push(`- [${c.Severity}] \`${c.ID}\` ${c.Label} — ${c.Detail}`);
|
|
2148
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
|
+
}
|
|
2149
2431
|
const env = m.environments || {};
|
|
2150
2432
|
const envLines = [];
|
|
2151
2433
|
if (env.vcpkg?.installed)
|
|
@@ -2193,6 +2475,7 @@ scan options:
|
|
|
2193
2475
|
--wsl-deep probe inside stopped WSL distros (default probes only running ones to avoid waking distros)
|
|
2194
2476
|
--ci CI gate: clean → exit 0; any warn → exit 2; any error → exit 1
|
|
2195
2477
|
--audit-deps dependency CVE audit via OSV-Scanner/pip-audit/govulncheck/Trivy (opt-in, uses network)
|
|
2478
|
+
--health-network run network health probes too (npm doctor / rustup check; off by default to stay offline-fast)
|
|
2196
2479
|
--quiet suppress scan progress feedback (for scripts/CI)
|
|
2197
2480
|
--bom prepend UTF-8 BOM for viewing in Notepad etc. (only on redirect/pipe)
|
|
2198
2481
|
--version / --help
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devmap
|
|
3
|
-
Version: 0.3.
|
|
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,6 +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, **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`.
|
|
43
44
|
- **Three sub-commands**: `scan` (the fact source), `query` (ask the manifest), `devdiff` (compare two manifests — environment drift / cross-machine consistency).
|
|
44
45
|
- **Zero Python dependencies.** The bundled app is pure TypeScript compiled to JavaScript.
|
|
45
46
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devmap"
|
|
7
|
-
version = "0.3.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|