devmap 0.3.6__py3-none-any.whl → 0.3.8__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
@@ -1,2386 +1,2573 @@
1
- #!/usr/bin/env node
2
- // @bun
3
-
4
- // src/version.ts
5
- var VERSION = "0.3.6";
6
-
7
- // src/install.ts
8
- function isIsolated(i) {
9
- return !i.onPath && !i.env;
10
- }
11
- function locationOf(i) {
12
- if (i.onPath)
13
- return "PATH";
14
- if (i.env)
15
- return "env";
16
- return "off-path";
17
- }
18
- function locationTag(i) {
19
- if (i.onPath)
20
- return "";
21
- return i.env ? `env: ${i.env}` : "off-path";
22
- }
23
-
24
- // src/detector/registry.ts
25
- var registry = [];
26
- function register(d) {
27
- registry.push(d);
28
- return d;
29
- }
30
- async function runDetectors(ctx, progress) {
31
- const byName = {};
32
- const allChecks = [];
33
- progress?.setTotal(registry.length);
34
- await Promise.all(registry.map(async (d) => {
35
- progress?.start(d.name);
36
- const t0 = Date.now();
37
- try {
38
- const r = await d.scan(ctx);
39
- byName[d.name] = r.data ?? {};
40
- if (r.checks && r.checks.length)
41
- allChecks.push(...r.checks);
42
- progress?.done(d.name, Date.now() - t0, false);
43
- } catch (e) {
44
- progress?.done(d.name, Date.now() - t0, true);
45
- byName[d.name] = { error: String(e?.message || e) };
46
- }
47
- }));
48
- return { byName, allChecks };
49
- }
50
-
51
- // src/check/model.ts
52
- function makeCheck(id, kind, severity, label, detail, path = [], fix) {
53
- const c = { ID: id, Kind: kind, Severity: severity, Label: label, Detail: detail, Path: path };
54
- if (fix)
55
- c.Fix = fix;
56
- return c;
57
- }
58
-
59
- // src/platform/exec.ts
60
- import { spawnSync, spawn } from "node:child_process";
61
- import { readdirSync, statSync, lstatSync, existsSync } from "node:fs";
62
- import { join, basename } from "node:path";
63
- var IS_WIN = process.platform === "win32";
64
- var PATH_SEP = IS_WIN ? ";" : ":";
65
- var PATHEXT = (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";").map((s) => s.toLowerCase());
66
- function decodeOutput(buf) {
67
- if (buf.length === 0)
68
- return "";
69
- if (buf.length >= 2 && buf[0] === 255 && buf[1] === 254)
70
- return buf.subarray(2).toString("utf16le");
71
- if (buf.length >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191)
72
- return buf.subarray(3).toString("utf8");
73
- const sample = Math.min(buf.length, 512);
74
- let odd = 0;
75
- let nulOdd = 0;
76
- for (let i = 1;i < sample; i += 2) {
77
- odd++;
78
- if (buf[i] === 0)
79
- nulOdd++;
80
- }
81
- if (odd >= 4 && nulOdd / odd > 0.5)
82
- return buf.toString("utf16le");
83
- return buf.toString("utf8");
84
- }
85
- function run(cmd, args = [], timeout = 5000) {
86
- try {
87
- const r = spawnSync(cmd, args, { encoding: "buffer", timeout, windowsHide: true });
88
- if (r.error)
89
- return "";
90
- const buf = Buffer.concat([r.stdout || Buffer.alloc(0), r.stderr || Buffer.alloc(0)]);
91
- return decodeOutput(buf).trim();
92
- } catch {
93
- return "";
94
- }
95
- }
96
- var MAX_PROC = 16;
97
- var activeProcs = 0;
98
- var procWaiters = [];
99
- function acquireProc() {
100
- if (activeProcs < MAX_PROC) {
101
- activeProcs++;
102
- return Promise.resolve();
103
- }
104
- return new Promise((resolve) => procWaiters.push(resolve)).then(() => {
105
- activeProcs++;
106
- });
107
- }
108
- function releaseProc() {
109
- activeProcs--;
110
- const next = procWaiters.shift();
111
- if (next)
112
- next();
113
- }
114
- async function runAsync(cmd, args = [], timeout = 5000) {
115
- await acquireProc();
116
- try {
117
- return await spawnCollect(cmd, args, timeout);
118
- } finally {
119
- releaseProc();
120
- }
121
- }
122
- function spawnCollect(cmd, args, timeout) {
123
- return new Promise((resolve) => {
124
- let settled = false;
125
- const finish = (s) => {
126
- if (settled)
127
- return;
128
- settled = true;
129
- resolve(s);
130
- };
131
- let child;
132
- try {
133
- child = spawn(cmd, args, { windowsHide: true });
134
- } catch {
135
- return finish("");
136
- }
137
- const chunks = [];
138
- child.stdout?.on("data", (d) => chunks.push(Buffer.from(d)));
139
- child.stderr?.on("data", (d) => chunks.push(Buffer.from(d)));
140
- child.stdout?.on("error", () => {});
141
- child.stderr?.on("error", () => {});
142
- const timer = setTimeout(() => {
143
- try {
144
- child.kill();
145
- } catch {}
146
- finish("");
147
- }, timeout);
148
- child.on("error", () => {
149
- clearTimeout(timer);
150
- finish("");
151
- });
152
- child.on("close", () => {
153
- clearTimeout(timer);
154
- finish(decodeOutput(Buffer.concat(chunks)).trim());
155
- });
156
- });
157
- }
158
- async function pool(items, limit, fn) {
159
- const results = new Array(items.length);
160
- let cursor = 0;
161
- const workers = new Array(Math.max(1, Math.min(limit, items.length))).fill(0).map(async () => {
162
- for (;; ) {
163
- const i = cursor++;
164
- if (i >= items.length)
165
- return;
166
- results[i] = await fn(items[i], i);
167
- }
168
- });
169
- await Promise.all(workers);
170
- return results;
171
- }
172
- function wrapBin(binPath, args) {
173
- if (IS_WIN && /\.(cmd|bat)$/i.test(binPath))
174
- return ["cmd", ["/c", binPath, ...args]];
175
- if (IS_WIN && /\.ps1$/i.test(binPath)) {
176
- const quoted = args.map((a) => `'${a.replace(/'/g, "''")}'`).join(" ");
177
- return ["powershell", ["-NoProfile", "-Command", `& '${binPath}' ${quoted}`]];
178
- }
179
- return [binPath, args];
180
- }
181
- function runBinAsync(binPath, args = [], timeout = 5000) {
182
- const [c, a] = wrapBin(binPath, args);
183
- return runAsync(c, a, timeout);
184
- }
185
- function pathDirs() {
186
- return (process.env.PATH || "").split(PATH_SEP).map((s) => s.trim()).filter(Boolean);
187
- }
188
- function hasExecExt(name) {
189
- if (!IS_WIN)
190
- return true;
191
- const lower = name.toLowerCase();
192
- return PATHEXT.some((ext) => lower.endsWith(ext));
193
- }
194
- var dirExecCache = new Map;
195
- function execEntriesOf(dir) {
196
- const key = IS_WIN ? dir.toLowerCase() : dir;
197
- const hit = dirExecCache.get(key);
198
- if (hit)
199
- return hit;
200
- const out = [];
201
- let entries = [];
202
- try {
203
- entries = readdirSync(dir);
204
- } catch {
205
- dirExecCache.set(key, out);
206
- return out;
207
- }
208
- for (const e of entries) {
209
- if (IS_WIN && !hasExecExt(e))
210
- continue;
211
- const full = join(dir, e);
212
- let st;
213
- try {
214
- st = statSync(full);
215
- } catch {
216
- continue;
217
- }
218
- if (!st.isFile())
219
- continue;
220
- if (!IS_WIN && !(st.mode & 73))
221
- continue;
222
- out.push({ name: IS_WIN ? e.toLowerCase() : e, full });
223
- }
224
- dirExecCache.set(key, out);
225
- return out;
226
- }
227
- function whichInDirs(name, dirs) {
228
- const out = [];
229
- const target = IS_WIN ? name.toLowerCase() : name;
230
- for (const dir of dirs) {
231
- for (const e of execEntriesOf(dir)) {
232
- if (IS_WIN) {
233
- if (e.name === target || PATHEXT.some((ext) => e.name === target + ext))
234
- out.push(e.full);
235
- } else if (e.name === target) {
236
- out.push(e.full);
237
- }
238
- }
239
- }
240
- return out;
241
- }
242
- var SEMVER_RE = /(?<![\d.])(\d+\.\d+\.\d+)(?:[-.+][0-9A-Za-z._-]+)*(?![\d.])/;
243
- var LOOSE_VERSION_RE = /(?<![\d.])(\d+\.\d+)(?:[-.+][0-9A-Za-z._-]+)*(?![\d.])/;
244
- function extractVersion(out, binPath) {
245
- const lines = out.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("["));
246
- if (binPath) {
247
- const self = basename(binPath).replace(/\.(exe|cmd|bat|ps1|sh)$/i, "").toLowerCase();
248
- const own = lines.find((l) => {
249
- const key = l.toLowerCase().replace(/^['"]/, "");
250
- return key.startsWith(self + ":") || key.startsWith(self + "':") || key.startsWith(self + '":');
251
- });
252
- if (own) {
253
- const m = own.match(SEMVER_RE) || own.match(LOOSE_VERSION_RE);
254
- if (m)
255
- return m[0];
256
- }
257
- }
258
- for (const l of lines) {
259
- const m = l.match(SEMVER_RE);
260
- if (m)
261
- return m[0];
262
- }
263
- for (const l of lines) {
264
- const m = l.match(LOOSE_VERSION_RE);
265
- if (m)
266
- return m[0];
267
- }
268
- return null;
269
- }
270
- async function versionOfAsync(binPath, flags, timeout = 9000) {
271
- for (const f of flags) {
272
- const out = await runBinAsync(binPath, [f], timeout);
273
- if (out) {
274
- const v = extractVersion(out, binPath);
275
- if (v)
276
- return v;
277
- }
278
- }
279
- return null;
280
- }
281
- function fileExists(p) {
282
- try {
283
- return existsSync(p);
284
- } catch {
285
- return false;
286
- }
287
- }
288
- function scanBrokenSymlinks(dirs) {
289
- const ghosts = [];
290
- for (const dir of dirs) {
291
- if (/[\\/]microsoft[\\/]windowsapps[\\/]?$/i.test(dir))
292
- continue;
293
- let entries = [];
294
- try {
295
- entries = readdirSync(dir);
296
- } catch {
297
- continue;
298
- }
299
- for (const e of entries) {
300
- const full = join(dir, e);
301
- let lstat;
302
- try {
303
- lstat = lstatSync(full);
304
- } catch {
305
- continue;
306
- }
307
- if (lstat.isSymbolicLink()) {
308
- try {
309
- statSync(full);
310
- } catch (err) {
311
- const code = err?.code;
312
- if (code === "ENOENT") {
313
- ghosts.push({ path: full, reason: "broken-symlink (target missing)" });
314
- }
315
- }
316
- }
317
- }
318
- }
319
- return ghosts;
320
- }
321
-
322
- // src/platform/shells.ts
323
- function shellName(s) {
324
- return { current: "current", powershell: "PowerShell", gitbash: "Git Bash", wsl: "WSL" }[s];
325
- }
326
- function shellPathDirs(shell) {
327
- if (shell === "current" || !IS_WIN)
328
- return pathDirs();
329
- try {
330
- if (shell === "powershell") {
331
- const out = run("powershell", [
332
- "-NoProfile",
333
- "-Command",
334
- "$p=[Environment]::GetEnvironmentVariable('PATH','Process');$u=[Environment]::GetEnvironmentVariable('PATH','User');$m=[Environment]::GetEnvironmentVariable('PATH','Machine'); ($p,$u,$m) -join ';'"
335
- ]);
336
- return (out || "").split(";").map((s) => s.trim()).filter(Boolean);
337
- }
338
- if (shell === "gitbash") {
339
- const out = run("bash", ["-lc", 'echo "$PATH"']);
340
- return (out || "").split(":").map((s) => s.trim()).filter(Boolean);
341
- }
342
- if (shell === "wsl") {
343
- const out = run("wsl", ["bash", "-lc", 'echo "$PATH"']);
344
- return (out || "").split(":").map((s) => s.trim()).filter(Boolean);
345
- }
346
- } catch {
347
- return pathDirs();
348
- }
349
- return pathDirs();
350
- }
351
-
352
- // src/detector/conflicts.ts
353
- function computeConflicts(groups, shells, disabled) {
354
- const checks = [];
355
- const multiVersionTools = [];
356
- const pathConflicts = [];
357
- const offPathNotes = [];
358
- const multiShell = shells.length > 1;
359
- for (const g of groups) {
360
- const distinctVersions = new Set(g.installs.map((i) => i.version || ""));
361
- if (g.installs.length > 1 && distinctVersions.size > 1) {
362
- multiVersionTools.push({ id: g.id, count: g.installs.length, versions: g.installs.map((i) => i.version) });
363
- checks.push(makeCheck(`version.multi.${g.id}`, "conflict", "warn", `${g.id} has ${g.installs.length} installs (${distinctVersions.size} distinct versions)`, `Versions: ${g.installs.map((i) => i.version || "?").join(", ")} (multiple versions installed, check which one is active)`, g.installs.map((i) => i.path)));
364
- }
365
- if (multiShell) {
366
- const res = {};
367
- for (const s of shells)
368
- res[s] = (whichInDirs(g.id, shellPathDirs(s)) || [])[0] || null;
369
- g.shellResolution = res;
370
- const vals = Object.values(res).filter(Boolean);
371
- if (new Set(vals).size > 1) {
372
- checks.push(makeCheck(`shell.mismatch.${g.id}`, "mismatch", "info", `${g.id} resolves differently across shells`, shells.map((s) => `${shellName(s)} → ${res[s] || "(not found)"}`).join(" | "), vals));
373
- }
374
- }
375
- const off = g.installs.filter(isIsolated);
376
- if (off.length) {
377
- const dedupe = Array.from(new Set(off.map((i) => i.path)));
378
- offPathNotes.push(...off.map((i) => ({ id: g.id, path: i.path })));
379
- checks.push(makeCheck(`path.off.${g.id}`, "mismatch", "info", `${g.id} is installed but not on the current shell's PATH`, `Not a version-manager environment, so nothing will activate it for you; add it to PATH manually to use it: ${dedupe.join(", ")}`, dedupe));
380
- }
381
- }
382
- for (const g of groups) {
383
- const seen = new Set;
384
- const unique = [];
385
- for (const i of g.installs) {
386
- if (!i.onPath)
387
- continue;
388
- const k = i.path.toLowerCase();
389
- if (seen.has(k))
390
- continue;
391
- seen.add(k);
392
- unique.push({ path: i.path, version: i.version || null });
393
- }
394
- if (unique.length > 1) {
395
- const versions = new Set(unique.map((u) => u.version || ""));
396
- const sameVersion = versions.size === 1;
397
- const resolvedTo = unique[0].path;
398
- const alsoAt = unique.slice(1).map((u) => u.path);
399
- if (!sameVersion) {
400
- pathConflicts.push({ name: g.id, resolvedTo, alsoAt });
401
- }
402
- checks.push(makeCheck(`path.duplicate.${g.id}`, "conflict", sameVersion ? "info" : "warn", sameVersion ? `${g.id} has ${unique.length} launcher entries on PATH (all ${unique[0].version || "same version"}; no version conflict)` : `${g.id}: version ${unique[0].version} shadows ${[...versions].slice(1).join(", ")} on PATH`, sameVersion ? `active → ${resolvedTo}; also at: ${alsoAt.join(", ")} — identical version, harmless redundancy` : `active → ${resolvedTo}; shadowed (different version): ${alsoAt.join(", ")}`, unique.map((u) => u.path)));
403
- }
404
- }
405
- const seenDir = new Set;
406
- const uniqueDirs = [];
407
- for (const d of pathDirs()) {
408
- const k = d.toLowerCase();
409
- if (seenDir.has(k))
410
- continue;
411
- seenDir.add(k);
412
- uniqueDirs.push(d);
413
- }
414
- const ghosts = scanBrokenSymlinks(uniqueDirs);
415
- const dirFreq = new Map;
416
- for (const d of pathDirs()) {
417
- const k = d.toLowerCase();
418
- dirFreq.set(k, (dirFreq.get(k) || 0) + 1);
419
- }
420
- const dupDirs = [...dirFreq.entries()].filter(([, c]) => c > 1).map(([d, c]) => ({ d, c }));
421
- if (dupDirs.length) {
422
- const redundant = dupDirs.reduce((s, x) => s + x.c - 1, 0);
423
- checks.push(makeCheck("path.duplicate-dir", "conflict", "info", `PATH contains ${dupDirs.length} duplicate directories (${redundant} redundant entries, amplifies shadowing)`, `Duplicates: ${dupDirs.map((x) => `${x.d} ×${x.c}`).join("; ")}`, dupDirs.map((x) => x.d)));
424
- }
425
- for (const gh of ghosts) {
426
- checks.push(makeCheck(`ghost.broken.${gh.path}`, "ghost", "warn", `Broken symlink found (ghost binary)`, `${gh.path} -- ${gh.reason}`, [gh.path]));
427
- }
428
- const filtered = disabled && disabled.size ? checks.filter((c) => !isDisabled(c.ID, disabled)) : checks;
429
- return { checks: filtered, multiVersionTools, pathConflicts, ghostBinaries: ghosts, offPathNotes };
430
- }
431
- function isDisabled(id, disabled) {
432
- for (const d of disabled)
433
- if (id === d || id.startsWith(d + ".") || id.startsWith(d))
434
- return true;
435
- return false;
436
- }
437
- function isCheckDisabled(id, disabled) {
438
- if (!disabled || !disabled.size)
439
- return false;
440
- return isDisabled(id, disabled);
441
- }
442
-
443
- // src/security/runtimeAdvisories.ts
444
- var RUNTIME_ADVISORIES = {
445
- node: {
446
- eolByVersion: {
447
- "16": "2023-09-11",
448
- "18": "2025-04-30",
449
- "20": "2026-04-30",
450
- "22": "2027-04-30",
451
- "24": "2028-04-30",
452
- "26": "2029-04-30"
453
- }
454
- },
455
- python: {
456
- eolByVersion: {
457
- "3.7": "2023-06-27",
458
- "3.8": "2024-10-07",
459
- "3.9": "2025-10-31",
460
- "3.10": "2026-10-31",
461
- "3.11": "2027-10-31",
462
- "3.12": "2028-10-31",
463
- "3.13": "2029-10-31"
464
- }
465
- },
466
- go: {
467
- eolByVersion: {
468
- "1.20": "2024-02-06",
469
- "1.21": "2024-08-13",
470
- "1.22": "2025-02-11",
471
- "1.23": "2025-08-12",
472
- "1.24": "2026-02-11"
473
- }
474
- }
475
- };
476
-
477
- // src/security/scan.ts
478
- function eolKeys(version) {
479
- const parts = version.split(".");
480
- const maj = parts[0];
481
- const min = parts[1] ?? "0";
482
- return [maj, `${maj}.${min}`];
483
- }
484
- function computeSecurity(byName) {
485
- const checks = [];
486
- const runtimes = byName.runtimes || [];
487
- const now = Date.now();
488
- for (const g of runtimes) {
489
- const adv = RUNTIME_ADVISORIES[g.id];
490
- if (!adv)
491
- continue;
492
- for (const inst of g.installs) {
493
- const v = inst.version;
494
- if (!v)
495
- continue;
496
- for (const key of eolKeys(v)) {
497
- const eol = adv.eolByVersion[key];
498
- if (eol) {
499
- const eolT = Date.parse(eol + "T00:00:00Z");
500
- if (!isNaN(eolT) && eolT < now) {
501
- checks.push(makeCheck(`security.eol.${g.id}`, "stale", "warn", `${g.id} ${v} is end-of-life (since ${eol})`, `${g.id} ${v} reached end-of-life on ${eol} and no longer receives security patches. Upgrade to a supported release. (${inst.path})`, [inst.path]));
502
- }
503
- break;
504
- }
505
- }
506
- }
507
- }
508
- return checks;
509
- }
510
-
511
- // src/security/sca.ts
512
- import { spawnSync as spawnSync2 } from "child_process";
513
- function whichTool(name) {
514
- const isWin = process.platform === "win32";
515
- const r = spawnSync2(isWin ? "where" : "command", isWin ? [name] : ["-v", name], { encoding: "utf-8" });
516
- if (r.error)
517
- return null;
518
- const out = (r.stdout || "").split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
519
- return out || null;
520
- }
521
- function getVersion(toolId) {
522
- const r = spawnSync2(toolId, ["--version"], { encoding: "utf-8", timeout: 15000 });
523
- if (r.error || !r.stdout)
524
- return null;
525
- const m = r.stdout.match(/(\d+\.\d+\.\d+)/);
526
- return m ? m[1] : null;
527
- }
528
- function toSeverity(s) {
529
- const t = (s || "").toUpperCase();
530
- if (t === "CRITICAL" || t === "HIGH")
531
- return "error";
532
- return "warn";
533
- }
534
- var SCA_TOOLS = [
535
- {
536
- id: "osv-scanner",
537
- label: "OSV-Scanner",
538
- auditArgs: ["--format", "json", "."],
539
- parse: (json) => {
540
- const out = [];
541
- for (const res of json?.results || []) {
542
- for (const v of res?.vulnerabilities || []) {
543
- const pkg = v?.package || {};
544
- const sev = toSeverity(v?.severity || v?.database_specific?.severity);
545
- const vid = v?.id || "UNKNOWN";
546
- out.push(makeCheck(`security.deps.osv.${vid}`, "vuln", sev, `${pkg.name || "?"}@${pkg.version || "?"} affected by ${vid} (${sev === "error" ? "HIGH" : "MEDIUM"})`, `${vid}: ${v?.summary || v?.details || ""}. Package ${pkg.name}@${pkg.version}.`, [pkg.name || ""]));
547
- }
548
- }
549
- return out;
550
- }
551
- },
552
- {
553
- id: "pip-audit",
554
- label: "pip-audit",
555
- auditArgs: ["-f", "json"],
556
- parse: (json) => {
557
- const out = [];
558
- const sevOf = (s) => s ? toSeverity(s) : "error";
559
- const seen = new Set;
560
- const push = (name, vid, sev, title, detail) => {
561
- const key = `${name}|${vid}`;
562
- if (seen.has(key))
563
- return;
564
- seen.add(key);
565
- out.push(makeCheck(`security.deps.pipaudit.${vid}`, "vuln", sev, title, detail, [name]));
566
- };
567
- for (const dep of json?.dependencies || []) {
568
- for (const v of dep?.vulns || []) {
569
- const sev = sevOf(v?.severity);
570
- const vid = v?.id || "UNKNOWN";
571
- push(dep?.name || "?", vid, sev, `${dep?.name || "?"}@${dep?.version || "?"} affected by ${vid} (${sev === "error" ? "HIGH" : "MEDIUM"})`, `${vid}: ${v?.description || ""}. Fix versions: ${(v?.fix_versions || []).join(", ") || "n/a"}.`);
572
- }
573
- }
574
- for (const v of json?.vulnerabilities || []) {
575
- const sev = sevOf(v?.severity);
576
- const vid = v?.id || "UNKNOWN";
577
- push(v?.name || "?", vid, sev, `${v?.name || "?"} affected by ${vid}`, `${vid}: ${v?.description || ""}.`);
578
- }
579
- return out;
580
- }
581
- },
582
- {
583
- id: "govulncheck",
584
- label: "govulncheck",
585
- auditArgs: ["-json", "."],
586
- parse: (json) => {
587
- const out = [];
588
- for (const res of json?.Results || []) {
589
- for (const v of res?.Vulnerabilities || []) {
590
- const score = v?.Severity?.Score;
591
- const sev = typeof score === "number" && score >= 9 ? "error" : "warn";
592
- const vid = v?.OSV?.id || "UNKNOWN";
593
- const pkg = v?.Package?.Path || v?.Package?.Name || "?";
594
- out.push(makeCheck(`security.deps.govulncheck.${vid}`, "vuln", sev, `${pkg}@${v?.Version || "?"} affected by ${vid} (${sev === "error" ? "HIGH" : "MEDIUM"})`, `${vid}: ${v?.Details || ""}. Package ${pkg}@${v?.Version || "?"}.`, [pkg]));
595
- }
596
- }
597
- return out;
598
- }
599
- },
600
- {
601
- id: "trivy",
602
- label: "Trivy",
603
- auditArgs: ["fs", "--format", "json", "."],
604
- parse: (json) => {
605
- const out = [];
606
- for (const res of json?.Results || []) {
607
- for (const v of res?.Vulnerabilities || []) {
608
- const sev = toSeverity(v?.Severity);
609
- const vid = v?.VulnerabilityID || "UNKNOWN";
610
- out.push(makeCheck(`security.deps.trivy.${vid}`, "vuln", sev, `${v?.PkgName || "?"}@${v?.InstalledVersion || "?"} affected by ${vid} (${sev === "error" ? "HIGH" : "MEDIUM"})`, `${vid}: ${v?.Title || v?.Description || ""}. Fixed in ${v?.FixedVersion || "n/a"}.`, [v?.PkgName || ""]));
611
- }
612
- }
613
- return out;
614
- }
615
- }
616
- ];
617
- function detectScaTools() {
618
- return SCA_TOOLS.map((t) => {
619
- const p = whichTool(t.id);
620
- return { id: t.id, label: t.label, path: p, version: p ? getVersion(t.id) : null };
621
- });
622
- }
623
- function computeScaInventory() {
624
- return detectScaTools().map((d) => {
625
- const id = `security.scanner.${d.id}`;
626
- if (d.path) {
627
- return makeCheck(id, "vuln", "info", `${d.label} available (${d.version || "?"})`, `${d.label} is installed at ${d.path}. devmap can shell out to it for dependency CVE audit with --audit-deps.`, [d.path || ""]);
628
- }
629
- return makeCheck(id, "vuln", "info", `${d.label} not installed`, `${d.label} not found on PATH. Install it to enable dependency CVE scanning via devmap --audit-deps (it fetches vuln DBs over the network).`, []);
630
- });
631
- }
632
- function runScaAudit(cwd) {
633
- const present = detectScaTools().filter((d) => d.path);
634
- if (present.length === 0) {
635
- return [
636
- makeCheck("security.deps.none", "vuln", "info", "no SCA tool installed — dependency CVE audit skipped", "Install osv-scanner / pip-audit / govulncheck / trivy to enable dependency CVE scanning via `devmap --audit-deps`.", [])
637
- ];
638
- }
639
- const checks = [];
640
- for (const d of present) {
641
- const tool = SCA_TOOLS.find((t) => t.id === d.id);
642
- const r = spawnSync2(d.id, tool.auditArgs, { cwd, encoding: "utf-8", timeout: 120000, maxBuffer: 16 * 1024 * 1024 });
643
- if (r.error || r.status === null) {
644
- checks.push(makeCheck(`security.deps.${d.id}.error`, "vuln", "info", `${d.label} audit failed to start`, `${d.label} could not be launched: ${r.error?.message || "unknown error"}.`, []));
645
- continue;
646
- }
647
- if (r.status !== 0 && !r.stdout.trim()) {
648
- checks.push(makeCheck(`security.deps.${d.id}.skip`, "vuln", "info", `${d.label} audit produced no output (exit ${r.status})`, `${d.label} returned no scannable result in ${cwd} (no project / no lockfile / no module).`, []));
649
- continue;
650
- }
651
- try {
652
- const json = JSON.parse(r.stdout);
653
- const parsed = tool.parse(json, d.label);
654
- checks.push(...parsed);
655
- if (parsed.length === 0) {
656
- checks.push(makeCheck(`security.deps.${d.id}.clean`, "vuln", "info", `${d.label} audit: no vulnerabilities found`, `${d.label} found no known vulnerabilities in ${cwd}.`, []));
657
- }
658
- } catch (e) {
659
- checks.push(makeCheck(`security.deps.${d.id}.parse`, "vuln", "info", `${d.label} audit output not JSON`, `${d.label} returned output that could not be parsed as JSON: ${e?.message || e}.`, []));
660
- }
661
- }
662
- return checks;
663
- }
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
-
723
- // 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
- },
739
- {
740
- id: "npm",
741
- label: "npm",
742
- kind: "mismatch",
743
- network: true,
744
- candidates: [["npm", ["doctor"]]],
745
- timeout: 20000,
746
- parse: parseNpmDoctor
747
- },
748
- {
749
- id: "rustup",
750
- label: "rustup",
751
- kind: "stale",
752
- network: true,
753
- candidates: [["rustup", ["check"]]],
754
- timeout: 15000,
755
- parse: parseRustupCheck
756
- }
757
- ];
758
- function pickCandidate(p) {
759
- const dirs = pathDirs();
760
- for (const [cmd, args] of p.candidates) {
761
- if (whichInDirs(cmd, dirs).length > 0)
762
- return [cmd, args];
763
- }
764
- return null;
765
- }
766
- var SEV_OF = {
767
- ok: "ok",
768
- warn: "warn",
769
- info: "info",
770
- skipped: "info"
771
- };
772
- function toCheck(p, r) {
773
- 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);
777
- }
778
- async function computeHealth(opts = {}) {
779
- const checks = [];
780
- const network = opts.healthNetwork === true;
781
- for (const p of PROBES) {
782
- const picked = pickCandidate(p);
783
- if (!picked)
784
- continue;
785
- if (p.network && !network) {
786
- 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
- continue;
788
- }
789
- const [cmd, args] = picked;
790
- const out = await runAsync(cmd, args, p.timeout);
791
- const result = p.parse(out);
792
- checks.push(toCheck(p, result));
793
- }
794
- return checks;
795
- }
796
-
797
- // src/output/json.ts
798
- function buildManifest(byName, conflicts, checks, environments, shells) {
799
- const sys = byName.system || {};
800
- const runtimes = byName.runtimes || [];
801
- const pkg = byName.packageManagers || [];
802
- const ides = byName.ides || [];
803
- const issueCountBySeverity = { error: 0, warn: 0, info: 0, missing: 0, ok: 0 };
804
- for (const c of checks)
805
- issueCountBySeverity[c.Severity] = (issueCountBySeverity[c.Severity] || 0) + 1;
806
- const totalInstallations = runtimes.reduce((a, g) => a + g.installs.length, 0) + pkg.reduce((a, g) => a + g.installs.length, 0) + ides.reduce((a, g) => a + g.installs.length, 0);
807
- const env = environments || {};
808
- const envSummary = {
809
- vcpkgPackages: env.vcpkg?.packages?.length || 0,
810
- pipInterpreters: env.pip?.perInterpreter?.length || 0,
811
- pipPackages: env.pip?.perInterpreter?.reduce((a, p) => a + p.packages.length, 0) || 0,
812
- pipOutdated: env.pip?.perInterpreter?.reduce((a, p) => a + p.outdated.length, 0) || 0,
813
- condaEnvs: env.condaEnvs?.envs?.length || 0,
814
- dockerInstalled: !!env.docker?.installed,
815
- wslDistros: env.wsl?.distros?.length || 0,
816
- vsShells: env.vsShells?.shells?.length || 0,
817
- projectEnvs: env.projectEnvs?.envs?.length || 0
818
- };
819
- return {
820
- meta: { tool: "devmap", version: VERSION, generatedAt: new Date().toISOString(), schema: "devmap.manifest/1.0", shells },
821
- system: sys,
822
- runtimes,
823
- packageManagers: pkg,
824
- ides,
825
- environments: env,
826
- checks,
827
- conflicts: {
828
- multiVersionTools: conflicts.multiVersionTools,
829
- pathConflicts: conflicts.pathConflicts,
830
- ghostBinaries: conflicts.ghostBinaries
831
- },
832
- summary: {
833
- runtimeKinds: runtimes.length,
834
- packageManagerKinds: pkg.length,
835
- ideKinds: ides.length,
836
- totalInstallations,
837
- toolsWithMultipleVersions: conflicts.multiVersionTools.length,
838
- issueCountBySeverity,
839
- environments: envSummary
840
- }
841
- };
842
- }
843
-
844
- // src/glyphs.ts
845
- var WIN = process.platform === "win32";
846
- var G = {
847
- spinner: WIN ? ["|", "/", "-", "\\"] : ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
848
- dot: WIN ? "|" : "·",
849
- arrow: WIN ? "->" : "→",
850
- check: WIN ? "[+]" : "✓",
851
- cross: WIN ? "[x]" : "✗",
852
- play: WIN ? "*" : "▶",
853
- mdash: WIN ? "-" : "—",
854
- hrule: WIN ? "-" : "─",
855
- scale: WIN ? "vs" : "⚖"
856
- };
857
- var MAP = [
858
- [/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/g, ""],
859
- [/🔍/g, ""],
860
- [/·/g, G.dot],
861
- [/→/g, G.arrow],
862
- [/—/g, G.mdash],
863
- [/─/g, G.hrule],
864
- [/⚖/g, G.scale],
865
- [/▶/g, G.play],
866
- [/✓/g, G.check],
867
- [/✗/g, G.cross]
868
- ];
869
- function clean(s) {
870
- if (!WIN)
871
- return s;
872
- let out = s;
873
- for (const [re, rep] of MAP)
874
- out = out.replace(re, rep);
875
- return out;
876
- }
877
-
878
- // src/progress.ts
879
- var SPINNERS = G.spinner;
880
-
881
- class ScanProgress {
882
- tty;
883
- total;
884
- doneCount = 0;
885
- started = Date.now();
886
- inflight = new Set;
887
- timings = [];
888
- timer = null;
889
- extra = "";
890
- constructor(total, tty = !!process.stderr.isTTY) {
891
- this.total = total;
892
- this.tty = tty;
893
- if (this.tty)
894
- this.timer = setInterval(() => this.render(), 110);
895
- }
896
- setTotal(n) {
897
- this.total = n;
898
- }
899
- sub(msg) {
900
- if (!this.tty) {
901
- process.stderr.write(" " + clean(msg) + `
902
- `);
903
- return;
904
- }
905
- this.extra = msg;
906
- this.render();
907
- }
908
- start(name) {
909
- this.inflight.add(name);
910
- if (!this.tty)
911
- process.stderr.write(`${G.play} ${clean(name)}
912
- `);
913
- else
914
- this.render();
915
- }
916
- done(name, ms, err = false) {
917
- this.inflight.delete(name);
918
- this.doneCount++;
919
- this.timings.push({ name, ms, err });
920
- if (!this.tty)
921
- process.stderr.write(`${err ? G.cross : G.check} ${clean(name)} (${(ms / 1000).toFixed(1)}s)
922
- `);
923
- else
924
- this.render();
925
- }
926
- availWidth() {
927
- const c = process.stderr.columns;
928
- return typeof c === "number" && c >= 40 ? c : 90;
929
- }
930
- render() {
931
- if (!this.tty)
932
- return;
933
- const t = ((Date.now() - this.started) / 1000).toFixed(1);
934
- const spin = SPINNERS[Math.floor(Date.now() / 110) % SPINNERS.length];
935
- const rawCur = [...this.inflight].join("+") + (this.extra ? G.dot + " " + this.extra : "") || G.mdash;
936
- let cur = clean(rawCur);
937
- const prefix = `${spin} scanning ${this.doneCount}/${this.total} ${G.dot} running: `;
938
- const suffix = ` ${G.dot} elapsed ${t}s`;
939
- const budget = this.availWidth() - prefix.length - suffix.length - 1;
940
- if (budget > 6 && cur.length > budget) {
941
- const ell = process.platform === "win32" ? "..." : "…";
942
- cur = cur.slice(0, Math.max(4, budget - ell.length)).trimEnd() + ell;
943
- }
944
- const msg = `${prefix}${cur}${suffix}`;
945
- process.stderr.write(`\r\x1B[K${msg}`);
946
- }
947
- finish() {
948
- if (this.timer) {
949
- clearInterval(this.timer);
950
- this.timer = null;
951
- }
952
- if (this.tty)
953
- process.stderr.write(`\r\x1B[K`);
954
- if (this.timings.length) {
955
- const w = Math.max(1, ...this.timings.map((t) => t.name.length));
956
- for (const t of this.timings)
957
- process.stderr.write(` ${t.err ? G.cross : G.check} ${clean(t.name).padEnd(w)} ${t.ms >= 1000 ? (t.ms / 1000).toFixed(1) + "s" : t.ms + "ms"}
958
- `);
959
- const total = ((Date.now() - this.started) / 1000).toFixed(1);
960
- process.stderr.write(` \x1B[2mscan complete, took ${total}s\x1B[0m
961
- `);
962
- }
963
- }
964
- }
965
-
966
- // src/scan.ts
967
- async function runScan(shells, disabled, opts = {}) {
968
- const progress = opts.silent ? undefined : new ScanProgress(0, process.stderr.isTTY);
969
- const ctx = {
970
- shells,
971
- disabled,
972
- projectRoots: opts.projectRoots,
973
- progress,
974
- checkOutdated: opts.checkOutdated,
975
- wslDeep: opts.wslDeep,
976
- auditDeps: opts.auditDeps,
977
- healthNetwork: opts.healthNetwork
978
- };
979
- const { byName, allChecks } = await runDetectors(ctx, progress);
980
- const groups = [
981
- ...byName.runtimes || [],
982
- ...byName.packageManagers || [],
983
- ...byName.ides || []
984
- ];
985
- const conflicts = computeConflicts(groups, shells, disabled);
986
- const healthChecks = await computeHealth({ healthNetwork: opts.healthNetwork });
987
- const securityChecks = [
988
- ...computeSecurity(byName),
989
- ...computeScaInventory(),
990
- ...opts.auditDeps ? runScaAudit(process.cwd()) : []
991
- ];
992
- const collected = [...allChecks, ...conflicts.checks, ...securityChecks, ...healthChecks].filter((c) => !isCheckDisabled(c.ID, disabled));
993
- const environments = {
994
- vcpkg: byName.vcpkg,
995
- pip: byName.pip,
996
- apt: byName.apt,
997
- condaEnvs: byName.condaenv,
998
- docker: byName.docker,
999
- wsl: byName.wsl,
1000
- vsShells: byName.vsshells,
1001
- projectEnvs: byName.projects
1002
- };
1003
- if (progress)
1004
- progress.finish();
1005
- return buildManifest(byName, conflicts, collected, environments, shells);
1006
- }
1007
-
1008
- // src/query.ts
1009
- var CAT_ALIASES = {
1010
- runtimes: "runtime",
1011
- runtime: "runtime",
1012
- rt: "runtime",
1013
- packagemanagers: "packageManager",
1014
- packagemanager: "packageManager",
1015
- pkg: "packageManager",
1016
- pm: "packageManager",
1017
- ides: "ide",
1018
- ide: "ide"
1019
- };
1020
- var CATEGORIES = [
1021
- ["runtimes", "runtime"],
1022
- ["packageManagers", "packageManager"],
1023
- ["ides", "ide"]
1024
- ];
1025
- function queryManifest(m, opts) {
1026
- let groups = [];
1027
- for (const [key, label] of CATEGORIES) {
1028
- for (const g of m[key] || []) {
1029
- groups.push({ id: g.id, category: label, installs: g.installs || [] });
1030
- }
1031
- }
1032
- let catFilter = opts.kind || null;
1033
- let idSub = (opts.selector || "").toLowerCase();
1034
- if (opts.selector && opts.selector.includes(".")) {
1035
- const dot = opts.selector.indexOf(".");
1036
- const catPart = opts.selector.slice(0, dot).toLowerCase();
1037
- const rest = opts.selector.slice(dot + 1).toLowerCase();
1038
- if (catPart in CAT_ALIASES) {
1039
- catFilter = CAT_ALIASES[catPart];
1040
- idSub = rest;
1041
- }
1042
- }
1043
- if (catFilter)
1044
- groups = groups.filter((g) => g.category === catFilter);
1045
- if (idSub)
1046
- groups = groups.filter((g) => g.id.toLowerCase().includes(idSub));
1047
- if (opts.where) {
1048
- const w = opts.where.toLowerCase();
1049
- groups = groups.map((g) => ({ ...g, installs: g.installs.filter((i) => (i.path || "").toLowerCase().includes(w)) })).filter((g) => g.installs.length > 0);
1050
- }
1051
- if (opts.offPath) {
1052
- groups = groups.map((g) => ({ ...g, installs: g.installs.filter(isIsolated) })).filter((g) => g.installs.length > 0);
1053
- }
1054
- let checks = [];
1055
- const matchesSelector = (c, s) => (c.ID || "").toLowerCase().includes(s) || (c.Label || "").toLowerCase().includes(s);
1056
- if (opts.issues) {
1057
- checks = m.checks || [];
1058
- if (idSub)
1059
- checks = checks.filter((c) => matchesSelector(c, idSub));
1060
- } else if (idSub) {
1061
- checks = (m.checks || []).filter((c) => matchesSelector(c, idSub));
1062
- }
1063
- const conflicts = opts.conflicts ? m.conflicts : undefined;
1064
- if (opts.issues || opts.conflicts)
1065
- groups = [];
1066
- return { groups, checks, conflicts };
1067
- }
1068
-
1069
- // src/diff.ts
1070
- var CAT_KEYS = [
1071
- ["runtimes", "runtime"],
1072
- ["packageManagers", "packageManager"],
1073
- ["ides", "ide"]
1074
- ];
1075
- function normPath(p) {
1076
- return (p || "").toLowerCase().replace(/\\/g, "/").replace(/\/+$/, "");
1077
- }
1078
- function collectInstalls(m) {
1079
- const map = new Map;
1080
- for (const [key, label] of CAT_KEYS) {
1081
- for (const g of m[key] || []) {
1082
- for (const inst of g.installs || []) {
1083
- map.set(normPath(inst.path), {
1084
- id: g.id,
1085
- category: label,
1086
- path: inst.path,
1087
- version: inst.version || null,
1088
- install: inst
1089
- });
1090
- }
1091
- }
1092
- }
1093
- return map;
1094
- }
1095
- function ensureTool(tools, id, cat) {
1096
- const k = id + "|" + cat;
1097
- let t = tools.get(k);
1098
- if (!t) {
1099
- t = { id, category: cat, added: [], removed: [], versionChanged: [] };
1100
- tools.set(k, t);
1101
- }
1102
- return t;
1103
- }
1104
- function diffManifests(a, b) {
1105
- const A = collectInstalls(a);
1106
- const B = collectInstalls(b);
1107
- const tools = new Map;
1108
- for (const [np, v] of B) {
1109
- const av = A.get(np);
1110
- const t = ensureTool(tools, v.id, v.category);
1111
- if (!av) {
1112
- t.added.push(v.install);
1113
- } else if (av.version !== v.version) {
1114
- t.versionChanged.push({ path: v.path, from: av.version, to: v.version });
1115
- }
1116
- }
1117
- for (const [np, v] of A) {
1118
- if (!B.has(np)) {
1119
- ensureTool(tools, v.id, v.category).removed.push(v.install);
1120
- }
1121
- }
1122
- const toolList = [...tools.values()].filter((t) => t.added.length || t.removed.length || t.versionChanged.length);
1123
- const aIssues = new Map((a.checks || []).map((c) => [c.ID, c]));
1124
- const bIssues = new Map((b.checks || []).map((c) => [c.ID, c]));
1125
- const issues = [];
1126
- for (const [id, c] of bIssues)
1127
- if (!aIssues.has(id))
1128
- issues.push({ id, added: [c], resolved: [] });
1129
- for (const [id, c] of aIssues)
1130
- if (!bIssues.has(id))
1131
- issues.push({ id, added: [], resolved: [c] });
1132
- const summary = {
1133
- installsAdded: toolList.reduce((s, t) => s + t.added.length, 0),
1134
- installsRemoved: toolList.reduce((s, t) => s + t.removed.length, 0),
1135
- versionChanges: toolList.reduce((s, t) => s + t.versionChanged.length, 0),
1136
- issuesAdded: issues.reduce((s, i) => s + i.added.length, 0),
1137
- issuesResolved: issues.reduce((s, i) => s + i.resolved.length, 0)
1138
- };
1139
- return { tools: toolList, issues, summary };
1140
- }
1141
-
1142
- // src/detector/system.ts
1143
- import os from "node:os";
1144
- register({
1145
- name: "system",
1146
- scan() {
1147
- const data = {
1148
- platform: process.platform,
1149
- arch: os.arch(),
1150
- hostname: os.hostname(),
1151
- release: os.release(),
1152
- shell: process.env.SHELL || (IS_WIN ? process.env.ComSpec || "cmd.exe" : "sh"),
1153
- totalMemGB: +(os.totalmem() / 1073741824).toFixed(1),
1154
- cpuCount: os.cpus().length,
1155
- pathDirCount: pathDirs().length
1156
- };
1157
- return { data, checks: [] };
1158
- }
1159
- });
1160
-
1161
- // src/detector/runtimes.ts
1162
- import { homedir } from "node:os";
1163
- import { join as join2 } from "node:path";
1164
- import { existsSync as existsSync2, readdirSync as readdirSync2 } from "node:fs";
1165
-
1166
- // src/detector/util.ts
1167
- var CONCURRENCY = 12;
1168
- var GENERIC_FLAGS = ["--version", "version", "-v", "-V"];
1169
- function dedupe(paths, seen) {
1170
- const out = [];
1171
- for (const p of paths) {
1172
- const k = p.toLowerCase();
1173
- if (seen.has(k))
1174
- continue;
1175
- seen.add(k);
1176
- out.push(p);
1177
- }
1178
- return out;
1179
- }
1180
- async function detectRuntimeAsync(id, bin, flags, multiFn) {
1181
- const seen = new Set;
1182
- const onPathPaths = dedupe(whichInDirs(bin, pathDirs()), seen);
1183
- const extras = [];
1184
- if (multiFn) {
1185
- for (const m of await multiFn()) {
1186
- if (!m.path)
1187
- continue;
1188
- const k = m.path.toLowerCase();
1189
- if (seen.has(k))
1190
- continue;
1191
- seen.add(k);
1192
- extras.push({ ...m, onPath: false });
1193
- }
1194
- }
1195
- const versions = await pool(onPathPaths, CONCURRENCY, (p) => versionOfAsync(p, flags));
1196
- const installs = onPathPaths.map((p, i) => ({ path: p, onPath: true, version: versions[i] }));
1197
- installs.push(...extras);
1198
- return installs.length ? { id, installs } : null;
1199
- }
1200
- async function detectSimpleAsync(list, onProgress) {
1201
- const dirs = pathDirs();
1202
- const byName = new Map;
1203
- const jobs = [];
1204
- for (const name of list) {
1205
- const paths = dedupe(whichInDirs(name, dirs), new Set);
1206
- if (!paths.length)
1207
- continue;
1208
- byName.set(name, paths);
1209
- for (const p of paths)
1210
- jobs.push({ name, path: p });
1211
- }
1212
- let done = 0;
1213
- const versions = await pool(jobs, CONCURRENCY, async (j) => {
1214
- const v = await versionOfAsync(j.path, GENERIC_FLAGS);
1215
- onProgress?.(++done, jobs.length);
1216
- return v;
1217
- });
1218
- const vmap = new Map;
1219
- jobs.forEach((j, i) => vmap.set(`${j.name}\x00${j.path}`, versions[i]));
1220
- const out = [];
1221
- for (const name of list) {
1222
- const paths = byName.get(name);
1223
- if (!paths)
1224
- continue;
1225
- out.push({
1226
- id: name,
1227
- installs: paths.map((p) => ({ path: p, onPath: true, version: vmap.get(`${name}\x00${p}`) ?? null }))
1228
- });
1229
- }
1230
- return out;
1231
- }
1232
-
1233
- // src/detector/runtimes.ts
1234
- var HOME = homedir();
1235
- async function condaPythonPaths() {
1236
- const found = [];
1237
- const bases = new Set;
1238
- const ci = await runAsync("conda", ["info", "--base"], 15000);
1239
- if (ci)
1240
- bases.add(ci.trim());
1241
- bases.add(join2(HOME, "miniconda3"));
1242
- bases.add(join2(HOME, "anaconda3"));
1243
- bases.add("E:\\Users\\Administrator\\miniconda3");
1244
- for (const base of bases) {
1245
- if (!base || !existsSync2(base))
1246
- continue;
1247
- const basePy = IS_WIN ? join2(base, "python.exe") : join2(base, "bin", "python");
1248
- if (existsSync2(basePy))
1249
- found.push({ path: basePy, env: "base" });
1250
- const envsDir = join2(base, "envs");
1251
- if (existsSync2(envsDir)) {
1252
- for (const e of readdirSync2(envsDir)) {
1253
- const py = IS_WIN ? join2(envsDir, e, "python.exe") : join2(envsDir, e, "bin", "python");
1254
- if (existsSync2(py))
1255
- found.push({ path: py, env: e });
1256
- }
1257
- }
1258
- }
1259
- return found;
1260
- }
1261
- async function scanCondaPythons() {
1262
- const paths = await condaPythonPaths();
1263
- const versions = await pool(paths, 12, (p) => versionOfAsync(p.path, ["--version", "-V"]));
1264
- return paths.map((p, i) => ({ path: p.path, onPath: false, version: versions[i], env: p.env }));
1265
- }
1266
- async function scanNvmNodes() {
1267
- const cands = [join2(HOME, "AppData", "Roaming", "nvm"), join2(HOME, ".nvm", "versions", "node")];
1268
- const found = [];
1269
- for (const dir of cands) {
1270
- if (!existsSync2(dir))
1271
- continue;
1272
- for (const v of readdirSync2(dir)) {
1273
- const bin = IS_WIN ? join2(dir, v, "node.exe") : join2(dir, v, "bin", "node");
1274
- if (existsSync2(bin))
1275
- found.push({ path: bin, env: v });
1276
- }
1277
- }
1278
- const versions = await pool(found, 12, (f) => versionOfAsync(f.path, ["--version"]));
1279
- return found.map((f, i) => ({ path: f.path, onPath: false, version: versions[i], env: f.env }));
1280
- }
1281
- async function scanKnownBinary(cands, flags) {
1282
- const hits = cands.filter((c) => existsSync2(c));
1283
- const versions = await pool(hits, 12, (c) => versionOfAsync(c, flags));
1284
- return hits.map((c, i) => ({ path: c, onPath: false, version: versions[i] }));
1285
- }
1286
- register({
1287
- name: "runtimes",
1288
- async scan(ctx) {
1289
- ctx.progress?.sub("runtimes · probing 7 runtimes concurrently");
1290
- const results = await Promise.all([
1291
- detectRuntimeAsync("node", "node", ["--version"], scanNvmNodes),
1292
- detectRuntimeAsync("python", "python", ["--version", "-V"], scanCondaPythons),
1293
- detectRuntimeAsync("go", "go", ["version"]),
1294
- detectRuntimeAsync("rustc", "rustc", ["--version"]),
1295
- detectRuntimeAsync("java", "java", ["-version"]),
1296
- detectRuntimeAsync("ruby", "ruby", ["--version"]),
1297
- detectRuntimeAsync("bun", "bun", ["--version"], () => scanKnownBinary([join2(HOME, ".bun", "bin", "bun"), join2(HOME, ".bun", "bin", "bun.exe")], ["--version"]))
1298
- ]);
1299
- const groups = results.filter((g) => g !== null);
1300
- return { data: groups, checks: [] };
1301
- }
1302
- });
1303
-
1304
- // src/detector/pkgmgrs.ts
1305
- var TOOLS = [
1306
- "npm",
1307
- "pip",
1308
- "pip3",
1309
- "conda",
1310
- "cargo",
1311
- "brew",
1312
- "winget",
1313
- "scoop",
1314
- "docker",
1315
- "kubectl",
1316
- "git",
1317
- "poetry",
1318
- "yarn",
1319
- "pnpm",
1320
- "pyenv",
1321
- "nvm"
1322
- ];
1323
- register({
1324
- name: "packageManagers",
1325
- async scan(ctx) {
1326
- const data = await detectSimpleAsync(TOOLS, (done, total) => {
1327
- ctx.progress?.sub(`packageManagers · ${done}/${total}`);
1328
- });
1329
- return { data, checks: [] };
1330
- }
1331
- });
1332
-
1333
- // src/detector/ides.ts
1334
- register({
1335
- name: "ides",
1336
- async scan(ctx) {
1337
- const data = await detectSimpleAsync(["code", "cursor", "pycharm", "idea"], (done, total) => {
1338
- ctx.progress?.sub(`ides · ${done}/${total}`);
1339
- });
1340
- return { data, checks: [] };
1341
- }
1342
- });
1343
-
1344
- // src/detector/vcpkg.ts
1345
- import { join as join3 } from "node:path";
1346
- var CANDIDATES = [
1347
- "C:\\vcpkg\\vcpkg.exe",
1348
- "C:\\src\\vcpkg\\vcpkg.exe",
1349
- "C:\\tools\\vcpkg\\vcpkg.exe",
1350
- "C:\\vcpkg\\vcpkg"
1351
- ];
1352
- function parseVcpkgVersion(out) {
1353
- if (!out)
1354
- return null;
1355
- const m = out.match(/version\s+(\d{4}-\d{2}-\d{2}(?:-[0-9a-f]+)?)/i);
1356
- return m ? m[1] : null;
1357
- }
1358
- async function findVcpkg() {
1359
- const cands = [];
1360
- const seen = new Set;
1361
- const add = (p, onPath) => {
1362
- const k = p.toLowerCase();
1363
- if (seen.has(k))
1364
- return;
1365
- seen.add(k);
1366
- cands.push({ path: p, onPath });
1367
- };
1368
- for (const c of CANDIDATES)
1369
- if (fileExists(c))
1370
- add(c, false);
1371
- if (process.env.VCPKG_ROOT) {
1372
- const p = join3(process.env.VCPKG_ROOT, IS_WIN ? "vcpkg.exe" : "vcpkg");
1373
- if (fileExists(p))
1374
- add(p, false);
1375
- }
1376
- for (const p of whichInDirs("vcpkg", pathDirs()))
1377
- add(p, true);
1378
- const versions = await pool(cands, 8, (c) => runBinAsync(c.path, ["version"], 9000).then(parseVcpkgVersion));
1379
- return cands.map((c, i) => ({ path: c.path, onPath: c.onPath, version: versions[i] }));
1380
- }
1381
- function parseList(out) {
1382
- const pkgs = [];
1383
- for (const line of out.split(`
1384
- `)) {
1385
- const m = line.match(/^(\S+?):(\S+)\s+(\S+)/);
1386
- if (m)
1387
- pkgs.push({ name: m[1], triplet: m[2], version: m[3] });
1388
- }
1389
- return pkgs;
1390
- }
1391
- register({
1392
- name: "vcpkg",
1393
- async scan() {
1394
- const installs = await findVcpkg();
1395
- const checks = [];
1396
- if (!installs.length)
1397
- return { data: { installed: false, installs: [], packages: [] }, checks };
1398
- const primary = installs[0].path;
1399
- const raw = await runBinAsync(primary, ["list"], 15000);
1400
- const fallback = raw ? "" : IS_WIN ? await runAsync("cmd", ["/c", primary, "list"], 15000) : await runAsync(primary, ["list"], 15000);
1401
- const packages = parseList(raw || fallback);
1402
- if (installs.length > 1) {
1403
- checks.push(makeCheck("version.multi.vcpkg", "conflict", "warn", `vcpkg has ${installs.length} installed copies`, `Multiple vcpkg copies can mix different triplet libraries; active → ${installs[0].path}; others: ${installs.slice(1).map((i) => i.path).join(", ")}`, installs.map((i) => i.path)));
1404
- }
1405
- return { data: { installed: true, installs, packages }, checks };
1406
- }
1407
- });
1408
-
1409
- // src/detector/piputil.ts
1410
- function parseList2(raw) {
1411
- if (!raw)
1412
- return [];
1413
- try {
1414
- const arr = JSON.parse(raw);
1415
- return Array.isArray(arr) ? arr.map((p) => ({ name: p.name, version: p.version || "?" })) : [];
1416
- } catch {
1417
- return [];
1418
- }
1419
- }
1420
- function parseOutdated(raw) {
1421
- if (!raw)
1422
- return [];
1423
- try {
1424
- const arr = JSON.parse(raw);
1425
- return Array.isArray(arr) ? arr.map((p) => ({ name: p.name, installed: p.version || "?", latest: p.latest_version || "?" })) : [];
1426
- } catch {
1427
- return [];
1428
- }
1429
- }
1430
- async function pipPackagesAsync(pythonPath, timeout = 6000, withOutdated = false) {
1431
- try {
1432
- const packages = parseList2(await runBinAsync(pythonPath, ["-m", "pip", "list", "--format", "json"], timeout));
1433
- let outdated = [];
1434
- if (withOutdated) {
1435
- outdated = parseOutdated(await runBinAsync(pythonPath, ["-m", "pip", "list", "--outdated", "--format", "json"], timeout));
1436
- }
1437
- return { packages, outdated };
1438
- } catch {
1439
- return { packages: [], outdated: [], error: "pip list failed" };
1440
- }
1441
- }
1442
-
1443
- // src/detector/pip.ts
1444
- import { basename as basename2 } from "node:path";
1445
- function shortName(pythonPath) {
1446
- const lower = pythonPath.toLowerCase();
1447
- const m = lower.match(/[\\/]envs[\\/]([^\\/]+)[\\/]/);
1448
- if (m)
1449
- return m[1];
1450
- if (/miniconda3[\\/]python/i.test(lower) || /anaconda3[\\/]python/i.test(lower))
1451
- return "base";
1452
- return basename2(pythonPath).replace(/\.exe$/i, "").replace(/\.cmd$/i, "");
1453
- }
1454
- async function findPythons() {
1455
- const found = [];
1456
- const seen = new Set;
1457
- const add = (p, onPath) => {
1458
- const k = p.toLowerCase();
1459
- if (seen.has(k))
1460
- return;
1461
- seen.add(k);
1462
- found.push({ path: p, onPath });
1463
- };
1464
- for (const p of whichInDirs("python", pathDirs()))
1465
- add(p, true);
1466
- for (const p of whichInDirs("python3", pathDirs()))
1467
- add(p, true);
1468
- for (const inst of await condaPythonPaths())
1469
- add(inst.path, false);
1470
- return found;
1471
- }
1472
- register({
1473
- name: "pip",
1474
- async scan(ctx) {
1475
- const pythons = await findPythons();
1476
- const perInterpreter = [];
1477
- const checks = [];
1478
- const withOutdated = ctx.checkOutdated === true;
1479
- let done = 0;
1480
- const results = await pool(pythons, 6, async (py) => {
1481
- const r = await pipPackagesAsync(py.path, withOutdated ? 20000 : 8000, withOutdated);
1482
- ctx.progress?.sub(`pip · ${++done}/${pythons.length} interpreters`);
1483
- return r;
1484
- });
1485
- pythons.forEach((py, i) => {
1486
- const r = results[i];
1487
- const short = shortName(py.path);
1488
- perInterpreter.push({ pythonPath: py.path, version: null, packages: r.packages, outdated: r.outdated });
1489
- if (r.outdated.length) {
1490
- checks.push(makeCheck(`pip.outdated.${short}`, "stale", "info", `python(${short}) has ${r.outdated.length} upgradable packages`, `e.g.: ${r.outdated.slice(0, 5).map((o) => `${o.name} ${o.installed}→${o.latest}`).join(", ")}`, [py.path]));
1491
- }
1492
- });
1493
- return { data: { perInterpreter }, checks };
1494
- }
1495
- });
1496
-
1497
- // src/detector/apt.ts
1498
- function parseApt(out) {
1499
- const pkgs = [];
1500
- for (const line of out.split(`
1501
- `)) {
1502
- const m = line.match(/^([^:\s]+)(?::[^\s]+)?\S*\s+(\S+)\s/);
1503
- if (m)
1504
- pkgs.push({ name: m[1], version: m[2] });
1505
- }
1506
- return pkgs;
1507
- }
1508
- register({
1509
- name: "apt",
1510
- async scan() {
1511
- const checks = [];
1512
- if (IS_WIN)
1513
- return { data: { available: false, count: 0, packages: [], upgradable: 0 }, checks };
1514
- const [installedRaw, upRaw] = await Promise.all([
1515
- runAsync("apt", ["list", "--installed"], 20000),
1516
- runAsync("apt", ["list", "--upgradable"], 15000)
1517
- ]);
1518
- const packages = installedRaw ? parseApt(installedRaw) : [];
1519
- const upgradable = upRaw ? parseApt(upRaw).length : 0;
1520
- if (upgradable > 0) {
1521
- checks.push(makeCheck("apt.upgradable", "stale", "info", `apt has ${upgradable} upgradable packages`, `Run sudo apt upgrade to update; or apt list --upgradable to view`, []));
1522
- }
1523
- return { data: { available: true, count: packages.length, packages, upgradable }, checks };
1524
- }
1525
- });
1526
-
1527
- // src/detector/condaenv.ts
1528
- import { join as join4 } from "node:path";
1529
- import { existsSync as existsSync3 } from "node:fs";
1530
- function pythonOf(envPath) {
1531
- const py = IS_WIN ? join4(envPath, "python.exe") : join4(envPath, "bin", "python");
1532
- return existsSync3(py) ? py : null;
1533
- }
1534
- register({
1535
- name: "condaenv",
1536
- async scan(ctx) {
1537
- const out = await runAsync("conda", ["env", "list"], 15000);
1538
- const checks = [];
1539
- if (!out)
1540
- return { data: { envs: [] }, checks };
1541
- const parsed = [];
1542
- for (const line of out.split(/\r?\n/)) {
1543
- const t = line.trim();
1544
- if (!t || t.startsWith("#"))
1545
- continue;
1546
- const m = t.match(/^(\S+)\s+(\*?)\s*(.+)$/);
1547
- if (!m)
1548
- continue;
1549
- parsed.push({ name: m[1], active: m[2] === "*", py: pythonOf(m[3].trim()) });
1550
- }
1551
- const withOutdated = ctx.checkOutdated === true;
1552
- let done = 0;
1553
- const results = await pool(parsed, 6, async (e) => {
1554
- const r = e.py ? await pipPackagesAsync(e.py, withOutdated ? 20000 : 8000, withOutdated) : { packages: [], outdated: [] };
1555
- ctx.progress?.sub(`conda env · ${++done}/${parsed.length}`);
1556
- return r;
1557
- });
1558
- const envs = [];
1559
- let anyOutdated = 0;
1560
- parsed.forEach((e, i) => {
1561
- const r = results[i];
1562
- anyOutdated += r.outdated.length;
1563
- envs.push({ name: e.name, pythonPath: e.py, active: e.active, packages: r.packages, outdated: r.outdated });
1564
- });
1565
- if (anyOutdated > 0) {
1566
- checks.push(makeCheck("conda.env.outdated", "stale", "info", `conda envs have ${anyOutdated} upgradable packages`, `Use conda env export / pip list --outdated to view per-env details`, envs.map((e) => e.pythonPath || e.name)));
1567
- }
1568
- return { data: { envs }, checks };
1569
- }
1570
- });
1571
-
1572
- // src/detector/docker.ts
1573
- var DESKTOP = "C:\\Program Files\\Docker\\Docker\\resources\\bin\\docker.exe";
1574
- function findDocker() {
1575
- for (const p of whichInDirs("docker", pathDirs()))
1576
- return p;
1577
- if (IS_WIN && fileExists(DESKTOP))
1578
- return DESKTOP;
1579
- return null;
1580
- }
1581
- function countLines(out) {
1582
- if (!out)
1583
- return 0;
1584
- return out.split(`
1585
- `).filter((l) => l.trim().length).length;
1586
- }
1587
- register({
1588
- name: "docker",
1589
- async scan() {
1590
- const checks = [];
1591
- const bin = findDocker();
1592
- if (!bin)
1593
- return { data: { installed: false, daemonRunning: null, engineVersion: null, images: 0, containers: 0 }, checks };
1594
- const [version, info] = await Promise.all([
1595
- versionOfAsync(bin, ["--version"]),
1596
- runBinAsync(bin, ["info", "--format", "{{.ServerVersion}}"], 8000)
1597
- ]);
1598
- const daemonRunning = !!info;
1599
- const engineVersion = info || null;
1600
- let images = 0;
1601
- let containers = 0;
1602
- if (daemonRunning) {
1603
- const [imgRaw, psRaw] = await Promise.all([
1604
- runBinAsync(bin, ["images", "-q"], 8000),
1605
- runBinAsync(bin, ["ps", "-a", "-q"], 8000)
1606
- ]);
1607
- images = countLines(imgRaw);
1608
- containers = countLines(psRaw);
1609
- }
1610
- if (!daemonRunning) {
1611
- checks.push(makeCheck("docker.daemon.down", "missing", "warn", "docker installed but daemon not running", "Docker Desktop not started or dockerd not running; images/containers cannot be enumerated", [bin]));
1612
- }
1613
- return { data: { installed: true, daemonRunning, engineVersion, images, containers }, checks };
1614
- }
1615
- });
1616
-
1617
- // src/detector/wsl.ts
1618
- var PROBE_SCRIPT = "echo py3=$(command -v python3); echo py=$(command -v python); echo node=$(command -v node); " + "echo go=$(command -v go); echo rustc=$(command -v rustc); echo gcc=$(command -v gcc); " + "echo git=$(command -v git); echo docker=$(command -v docker)";
1619
- var TOOL_KEYS = ["py3", "py", "node", "go", "rustc", "gcc", "git", "docker"];
1620
- async function probeDistro(name) {
1621
- const tools = {};
1622
- for (const k of TOOL_KEYS)
1623
- tools[k] = null;
1624
- const out = await runAsync("wsl", ["-d", name, "--", "bash", "-lc", PROBE_SCRIPT], 20000);
1625
- if (!out)
1626
- return tools;
1627
- for (const line of out.split(/\r?\n/)) {
1628
- const m = line.trim().match(/^(\w+)=(.*)$/);
1629
- if (m && m[1] in tools)
1630
- tools[m[1]] = m[2].trim() || null;
1631
- }
1632
- return tools;
1633
- }
1634
- async function distroBasePaths() {
1635
- const map = new Map;
1636
- if (!IS_WIN)
1637
- return map;
1638
- const root = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Lxss";
1639
- const keysRaw = await runAsync("reg", ["query", root], 5000);
1640
- if (!keysRaw)
1641
- return map;
1642
- const keys = keysRaw.split(/\r?\n/).map((l) => l.trim()).filter((k) => /\\\{[0-9a-fA-F-]+\}$/.test(k));
1643
- const details = await pool(keys, 8, (k) => runAsync("reg", ["query", k], 5000));
1644
- for (const detail of details) {
1645
- if (!detail)
1646
- continue;
1647
- const nameM = detail.match(/DistributionName\s+REG_SZ\s+(.+)/);
1648
- const baseM = detail.match(/BasePath\s+REG_SZ\s+(.+)/);
1649
- if (nameM)
1650
- map.set(nameM[1].trim(), baseM ? baseM[1].trim() : "");
1651
- }
1652
- return map;
1653
- }
1654
- function parseList3(raw) {
1655
- const distros = [];
1656
- for (const line of raw.split(/\r?\n/)) {
1657
- const t = line.replace(/\u0000/g, "").trimEnd();
1658
- if (!t.trim())
1659
- continue;
1660
- if (/^\s*\*?\s*NAME\s+STATE\s+VERSION/i.test(t))
1661
- continue;
1662
- if (/^[\s-]+$/.test(t))
1663
- continue;
1664
- const m = t.match(/^(\*?)\s*(\S.*?)\s{2,}(\S+)\s+(\d+)\s*$/);
1665
- if (!m)
1666
- continue;
1667
- distros.push({ name: m[2].trim(), state: m[3].trim(), version: m[4], default: m[1] === "*" });
1668
- }
1669
- return distros;
1670
- }
1671
- register({
1672
- name: "wsl",
1673
- async scan(ctx) {
1674
- const checks = [];
1675
- if (!IS_WIN)
1676
- return { data: { available: false, distros: [] }, checks };
1677
- const raw = await runAsync("wsl", ["-l", "-v"], 8000);
1678
- if (!raw)
1679
- return { data: { available: false, distros: [] }, checks };
1680
- const distros = parseList3(raw);
1681
- const bases = await distroBasePaths();
1682
- for (const d of distros) {
1683
- const bp = bases.get(d.name);
1684
- if (bp)
1685
- d.basePath = bp;
1686
- }
1687
- const deep = ctx.wslDeep === true;
1688
- const targets = distros.filter((d) => deep || /running/i.test(d.state));
1689
- if (targets.length) {
1690
- ctx.progress?.sub(`wsl · probing ${targets.length} distros`);
1691
- const toolsList = await pool(targets, 4, (d) => probeDistro(d.name));
1692
- targets.forEach((d, i) => {
1693
- d.tools = toolsList[i];
1694
- d.probed = true;
1695
- });
1696
- }
1697
- for (const d of distros)
1698
- if (!d.probed)
1699
- d.probed = false;
1700
- if (distros.length && !distros.some((d) => d.default)) {
1701
- checks.push(makeCheck("wsl.no-default", "mismatch", "info", "WSL has no default distro", "Set one with wsl --set-default <name>", distros.map((d) => d.name)));
1702
- }
1703
- const skipped = distros.filter((d) => !d.probed);
1704
- if (skipped.length) {
1705
- checks.push(makeCheck("wsl.not-probed", "mismatch", "info", `${skipped.length} WSL distros were not probed for internal tools (not running)`, "Stopped distros are not woken up by default, to keep the scan fast; pass --wsl-deep to probe them all", skipped.map((d) => `${d.name}(${d.state})`)));
1706
- }
1707
- return { data: { available: true, distros }, checks };
1708
- }
1709
- });
1710
-
1711
- // src/detector/vsshells.ts
1712
- import { readdirSync as readdirSync3, existsSync as existsSync4, statSync as statSync2 } from "node:fs";
1713
- import { join as join5 } from "node:path";
1714
- var VS_BASES = [
1715
- "C:\\Program Files\\Microsoft Visual Studio",
1716
- "C:\\Program Files (x86)\\Microsoft Visual Studio"
1717
- ];
1718
- var START_MENU = [
1719
- "C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs",
1720
- "C:\\Users\\Administrator\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs"
1721
- ];
1722
- var EDITIONS = ["Community", "Professional", "Enterprise", "BuildTools"];
1723
- function vsRoots() {
1724
- const roots = [];
1725
- for (const b of VS_BASES) {
1726
- if (!existsSync4(b))
1727
- continue;
1728
- for (const v of readdirSync3(b)) {
1729
- const verDir = join5(b, v);
1730
- if (!existsSync4(verDir) || !statSync2(verDir).isDirectory())
1731
- continue;
1732
- const hasEdition = EDITIONS.some((e) => {
1733
- const ed = join5(verDir, e);
1734
- return existsSync4(ed) && statSync2(ed).isDirectory();
1735
- });
1736
- if (hasEdition) {
1737
- for (const e of EDITIONS) {
1738
- const ed = join5(verDir, e);
1739
- if (existsSync4(ed) && statSync2(ed).isDirectory())
1740
- roots.push(ed);
1741
- }
1742
- } else {
1743
- roots.push(verDir);
1744
- }
1745
- }
1746
- }
1747
- return roots;
1748
- }
1749
- function msvcVersionsDesc(msvc) {
1750
- let vers = [];
1751
- try {
1752
- vers = readdirSync3(msvc);
1753
- } catch {
1754
- return [];
1755
- }
1756
- return vers.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
1757
- }
1758
- function vsCl(root, arch) {
1759
- const msvc = join5(root, "VC", "Tools", "MSVC");
1760
- if (!existsSync4(msvc))
1761
- return null;
1762
- for (const v of msvcVersionsDesc(msvc)) {
1763
- const p = join5(msvc, v, "bin", `Host${arch}`, arch, "cl.exe");
1764
- if (existsSync4(p))
1765
- return p;
1766
- }
1767
- return null;
1768
- }
1769
- function vsMsbuild(root) {
1770
- const base = join5(root, "MSBuild");
1771
- if (!existsSync4(base))
1772
- return null;
1773
- const cands = [join5(base, "Current", "Bin", "MSBuild.exe")];
1774
- try {
1775
- for (const v of readdirSync3(base))
1776
- cands.push(join5(base, v, "Bin", "MSBuild.exe"));
1777
- } catch {}
1778
- return cands.find((c) => existsSync4(c)) || null;
1779
- }
1780
- function vsCMake(root) {
1781
- const p = join5(root, "Common7", "IDE", "CommonExtensions", "Microsoft", "CMake", "CMake", "bin", "cmake.exe");
1782
- return existsSync4(p) ? p : null;
1783
- }
1784
- function vsDotnet() {
1785
- return whichInDirs("dotnet", pathDirs())[0] || null;
1786
- }
1787
- function scanToolchain() {
1788
- const out = { cl_x64: null, cl_x86: null, cl_arm64: null, msbuild: null, cmake: null, dotnet: vsDotnet() };
1789
- for (const r of vsRoots()) {
1790
- if (!out.cl_x64)
1791
- out.cl_x64 = vsCl(r, "x64");
1792
- if (!out.cl_x86)
1793
- out.cl_x86 = vsCl(r, "x86");
1794
- if (!out.cl_arm64)
1795
- out.cl_arm64 = vsCl(r, "arm64");
1796
- if (!out.msbuild)
1797
- out.msbuild = vsMsbuild(r);
1798
- if (!out.cmake)
1799
- out.cmake = vsCMake(r);
1800
- }
1801
- return out;
1802
- }
1803
- function shortcutNames() {
1804
- const names = [];
1805
- const stack = [...START_MENU.filter((d) => existsSync4(d))];
1806
- while (stack.length) {
1807
- const dir = stack.pop();
1808
- let entries = [];
1809
- try {
1810
- entries = readdirSync3(dir);
1811
- } catch {
1812
- continue;
1813
- }
1814
- for (const e of entries) {
1815
- const full = join5(dir, e);
1816
- try {
1817
- if (statSync2(full).isDirectory())
1818
- stack.push(full);
1819
- else if (e.toLowerCase().endsWith(".lnk") && /developer|native tools|vs 20/i.test(e))
1820
- names.push(e.replace(/\.lnk$/i, ""));
1821
- } catch {}
1822
- }
1823
- }
1824
- return names;
1825
- }
1826
- function inferKindArch(name) {
1827
- const kind = /powershell/i.test(name) ? "powershell" : "command";
1828
- const am = name.match(/x64|x86|arm64/i);
1829
- const arch = am ? am[0].toLowerCase() : "auto";
1830
- return { kind, arch };
1831
- }
1832
- function entryRef(kind) {
1833
- const fn = kind === "powershell" ? "Launch-VsDevShell.ps1" : "VsDevCmd.bat";
1834
- for (const r of vsRoots()) {
1835
- const p = join5(r, "Common7", "Tools", fn);
1836
- if (existsSync4(p))
1837
- return p;
1838
- }
1839
- return null;
1840
- }
1841
- register({
1842
- name: "vsshells",
1843
- scan() {
1844
- const checks = [];
1845
- if (!IS_WIN)
1846
- return { data: { shells: [] }, checks };
1847
- const tc = scanToolchain();
1848
- const names = shortcutNames();
1849
- const shells = [];
1850
- const seen = new Set;
1851
- const anyCl = !!(tc.cl_x64 || tc.cl_x86 || tc.cl_arm64);
1852
- for (const name of names) {
1853
- const { kind, arch } = inferKindArch(name);
1854
- const key = `${kind}:${arch}`;
1855
- if (seen.has(key))
1856
- continue;
1857
- seen.add(key);
1858
- const clPath = arch === "auto" ? tc.cl_x64 : tc[`cl_${arch}`];
1859
- const tools = {
1860
- cl: clPath,
1861
- msbuild: tc.msbuild,
1862
- cmake: tc.cmake,
1863
- dotnet: tc.dotnet
1864
- };
1865
- shells.push({ name, kind, arch, entry: entryRef(kind), pathDirs: Object.values(tools).filter(Boolean).length, tools });
1866
- }
1867
- if (shells.length) {
1868
- checks.push(makeCheck("vs.shells", "mismatch", "info", `Found ${shells.length} Visual Studio dev shells`, shells.map((s) => s.name).join("; "), shells.map((s) => s.entry || s.name)));
1869
- }
1870
- if (shells.length && !anyCl) {
1871
- checks.push(makeCheck("vs.no-vc-tools", "missing", "warn", 'Visual Studio is installed but no cl.exe was found (the "Desktop development with C++" workload may be missing)', "Dev shells will have no MSVC toolchain; select the C++ workload in the Visual Studio Installer", vsRoots()));
1872
- }
1873
- return { data: { shells }, checks };
1874
- }
1875
- });
1876
-
1877
- // src/detector/projects.ts
1878
- 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";
1880
- function isVenv(dir) {
1881
- if (!existsSync5(join6(dir, "pyvenv.cfg")))
1882
- return false;
1883
- const child = IS_WIN_PYVENV(dir);
1884
- return child;
1885
- }
1886
- function IS_WIN_PYVENV(dir) {
1887
- return existsSync5(join6(dir, "Scripts", "activate")) || existsSync5(join6(dir, "Scripts", "activate.bat")) || existsSync5(join6(dir, "bin", "activate"));
1888
- }
1889
- function walk(root, maxDepth, out, seen) {
1890
- const stack = [[root, 0]];
1891
- while (stack.length) {
1892
- const [dir, depth] = stack.pop();
1893
- let entries = [];
1894
- try {
1895
- entries = readdirSync4(dir);
1896
- } catch {
1897
- continue;
1898
- }
1899
- for (const e of entries) {
1900
- if (e === "node_modules" || e === ".git" || e === "target" || e === "dist" || e === ".next" || e === "out") {
1901
- if (e === "node_modules") {
1902
- const p = join6(dir, e);
1903
- if (!seen.has(p.toLowerCase())) {
1904
- seen.add(p.toLowerCase());
1905
- out.push({ project: basename3(dir), type: "node_modules", path: p });
1906
- }
1907
- }
1908
- continue;
1909
- }
1910
- const full = join6(dir, e);
1911
- let st;
1912
- try {
1913
- st = statSync3(full);
1914
- } catch {
1915
- continue;
1916
- }
1917
- if (!st.isDirectory())
1918
- continue;
1919
- if (e === ".venv" || e === "venv") {
1920
- if (isVenv(full) && !seen.has(full.toLowerCase())) {
1921
- seen.add(full.toLowerCase());
1922
- out.push({ project: basename3(dirname(full)), type: "venv", path: full });
1923
- }
1924
- continue;
1925
- }
1926
- if (depth < maxDepth)
1927
- stack.push([full, depth + 1]);
1928
- }
1929
- }
1930
- }
1931
- register({
1932
- name: "projects",
1933
- scan(ctx) {
1934
- const roots = ctx.projectRoots && ctx.projectRoots.length ? ctx.projectRoots : [process.cwd()];
1935
- const envs = [];
1936
- const seen = new Set;
1937
- for (const r of roots) {
1938
- if (existsSync5(r))
1939
- walk(r, 4, envs, seen);
1940
- }
1941
- const checks = [];
1942
- if (envs.length) {
1943
- const venv = envs.filter((e) => e.type === "venv").length;
1944
- const nm = envs.filter((e) => e.type === "node_modules").length;
1945
- checks.push(makeCheck("project.envs", "mismatch", "info", `Found ${envs.length} project-local environment${envs.length === 1 ? "" : "s"} across ${roots.length} root director${roots.length === 1 ? "y" : "ies"} (venv×${venv} / node_modules×${nm})`, envs.slice(0, 10).map((e) => `${e.project}:${e.type}`).join(", "), envs.map((e) => e.path)));
1946
- }
1947
- return { data: { roots, envs }, checks };
1948
- }
1949
- });
1950
-
1951
- // src/cli.ts
1952
- import { writeFileSync, mkdirSync, readFileSync, existsSync as existsSync6 } from "node:fs";
1953
- import { homedir as homedir2 } from "node:os";
1954
- import { join as join7 } from "node:path";
1955
- import { execSync } from "node:child_process";
1956
- if (process.platform === "win32") {
1957
- try {
1958
- execSync("chcp 65001 > nul 2>&1", { stdio: "ignore" });
1959
- } catch {}
1960
- }
1961
- if (process.platform === "win32") {
1962
- const olog = console.log.bind(console);
1963
- console.log = (...a) => olog(...a.map((x) => typeof x === "string" ? clean(x) : x));
1964
- const oerr = console.error.bind(console);
1965
- console.error = (...a) => oerr(...a.map((x) => typeof x === "string" ? clean(x) : x));
1966
- }
1967
- var ALL_SHELLS = ["current", "powershell", "gitbash", "wsl"];
1968
- var DEFAULT_MANIFEST = join7(homedir2(), ".devmap", "devmap.json");
1969
- function emitText(text, withBom = false) {
1970
- const prefix = withBom && !process.stdout.isTTY ? "\uFEFF" : "";
1971
- process.stdout.write(prefix + text + `
1972
- `);
1973
- }
1974
- function tokenize(argv) {
1975
- const pos = [];
1976
- const opts = {};
1977
- for (let i = 0;i < argv.length; i++) {
1978
- const a = argv[i];
1979
- if (a.startsWith("--")) {
1980
- const eq = a.indexOf("=");
1981
- if (eq >= 0) {
1982
- opts[a.slice(2, eq)] = a.slice(eq + 1);
1983
- continue;
1984
- }
1985
- const key = a.slice(2);
1986
- const nxt = argv[i + 1];
1987
- if (nxt !== undefined && !nxt.startsWith("--")) {
1988
- opts[key] = opts[key] !== undefined ? `${opts[key]},${nxt}` : nxt;
1989
- i++;
1990
- } else
1991
- opts[key] = true;
1992
- } else if (a.startsWith("-") && a.length > 1) {
1993
- opts[a.slice(1)] = true;
1994
- } else
1995
- pos.push(a);
1996
- }
1997
- return { pos, opts };
1998
- }
1999
- function toShells(v) {
2000
- if (typeof v !== "string")
2001
- return ["current"];
2002
- const list = v.split(",").map((s) => s.trim());
2003
- return list.includes("all") ? ALL_SHELLS : list.length ? list : ["current"];
2004
- }
2005
- function toSet(v) {
2006
- if (typeof v !== "string")
2007
- return new Set;
2008
- return new Set(v.split(",").map((s) => s.trim()).filter(Boolean));
2009
- }
2010
- function toRoots(v) {
2011
- if (typeof v !== "string")
2012
- return;
2013
- const arr = v.split(",").map((s) => s.trim()).filter(Boolean);
2014
- return arr.length ? arr : undefined;
2015
- }
2016
- function readManifestFile(path) {
2017
- if (!existsSync6(path))
2018
- throw new Error(`manifest file not found: ${path}`);
2019
- try {
2020
- const raw = readFileSync(path, "utf8").replace(/^\uFEFF/, "");
2021
- return JSON.parse(raw);
2022
- } catch (e) {
2023
- throw new Error(`manifest parse failed (${path}): ${e.message}`);
2024
- }
2025
- }
2026
- async function resolveManifest(explicitPath, shells, disabled, label, roots) {
2027
- if (explicitPath)
2028
- return { manifest: readManifestFile(explicitPath), source: explicitPath };
2029
- if (existsSync6(DEFAULT_MANIFEST))
2030
- return { manifest: readManifestFile(DEFAULT_MANIFEST), source: DEFAULT_MANIFEST };
2031
- const manifest = await runScan(shells, disabled, { projectRoots: roots });
2032
- return { manifest, source: label + "(live scan)" };
2033
- }
2034
- async function cmdScan(opts, pos) {
2035
- const shells = toShells(opts.shell);
2036
- const disabled = toSet(opts.disable);
2037
- const roots = toRoots(opts["scan-root"]);
2038
- const manifest = await runScan(shells, disabled, {
2039
- projectRoots: roots,
2040
- silent: opts.quiet === true,
2041
- checkOutdated: opts.outdated === true,
2042
- wslDeep: opts["wsl-deep"] === true,
2043
- auditDeps: opts["audit-deps"] === true,
2044
- healthNetwork: opts["health-network"] === true
2045
- });
2046
- if (opts.json) {
2047
- emitText(JSON.stringify(manifest, null, 2), opts.bom === true);
2048
- } else if (opts.report) {
2049
- emitText(clean(renderMarkdownLite(manifest)), opts.bom === true);
2050
- } else {
2051
- renderPrettyFromManifest(manifest);
2052
- if (opts["no-manifest"] !== true) {
2053
- try {
2054
- mkdirSync(join7(DEFAULT_MANIFEST, ".."), { recursive: true });
2055
- writeFileSync(DEFAULT_MANIFEST, JSON.stringify(manifest, null, 2));
2056
- console.log(` ${"\x1B[2m"}manifest → ${DEFAULT_MANIFEST}${"\x1B[0m"}`);
2057
- } catch {}
2058
- }
2059
- }
2060
- if (opts.ci) {
2061
- const hasError = (manifest.checks || []).some((c) => c.Severity === "error");
2062
- if (hasError)
2063
- return 1;
2064
- const hasWarn = (manifest.checks || []).some((c) => c.Severity === "warn");
2065
- if (hasWarn)
2066
- return 2;
2067
- return 0;
2068
- }
2069
- return 0;
2070
- }
2071
- async function cmdQuery(opts, pos) {
2072
- const selector = pos[1];
2073
- const shells = toShells(opts.shell);
2074
- const disabled = toSet(opts.disable);
2075
- const { manifest, source } = await resolveManifest(opts.manifest, shells, disabled, "query", toRoots(opts["scan-root"]));
2076
- const result = queryManifest(manifest, {
2077
- selector,
2078
- issues: opts.issues === true,
2079
- conflicts: opts.conflicts === true,
2080
- offPath: opts["off-path"] === true || opts.offpath === true,
2081
- kind: opts.kind,
2082
- where: opts.where
2083
- });
2084
- if (opts.json) {
2085
- emitText(JSON.stringify({ source, ...result }, null, 2), opts.bom === true);
2086
- return 0;
2087
- }
2088
- console.log(`\x1B[1m\uD83D\uDD0D query:\x1B[0m ${selector || "(all)"} \x1B[2m(source: ${source})\x1B[0m`);
2089
- for (const g of result.groups) {
2090
- console.log(` \x1B[36m${g.category}\x1B[0m ${g.id} \x1B[2m(${g.installs.length})\x1B[0m`);
2091
- for (const inst of g.installs) {
2092
- const loc = locationOf(inst);
2093
- const tag = loc === "PATH" ? "\x1B[32m[onPath]\x1B[0m" : loc === "env" ? `\x1B[36m[env: ${inst.env}]\x1B[0m` : "\x1B[33m[off-path]\x1B[0m";
2094
- console.log(` ${inst.path} \x1B[2m${inst.version || "?"}\x1B[0m ${tag}`);
2095
- }
2096
- }
2097
- if (result.conflicts) {
2098
- const c = result.conflicts;
2099
- console.log(` \x1B[1mconflicts\x1B[0m multiVersion=${c.multiVersionTools?.length || 0} path=${c.pathConflicts?.length || 0} ghost=${c.ghostBinaries?.length || 0}`);
2100
- }
2101
- if (result.checks && result.checks.length) {
2102
- console.log(` \x1B[1missues (${result.checks.length})\x1B[0m`);
2103
- for (const c of result.checks) {
2104
- const sev = sevColor(c.Severity);
2105
- console.log(` ${sev}${c.Severity}\x1B[0m ${c.ID} ${c.Label}`);
2106
- }
2107
- }
2108
- if (!result.groups.length && !result.checks.length && !result.conflicts) {
2109
- console.log(" (no match)");
2110
- }
2111
- return 0;
2112
- }
2113
- async function cmdDevdiff(opts, pos) {
2114
- const shells = toShells(opts.shell);
2115
- const disabled = toSet(opts.disable);
2116
- const files = pos.slice(1).filter((p) => !p.startsWith("-"));
2117
- let A, labelA;
2118
- let B, labelB;
2119
- if (files.length >= 2) {
2120
- A = readManifestFile(files[0]);
2121
- labelA = files[0];
2122
- B = readManifestFile(files[1]);
2123
- labelB = files[1];
2124
- } else if (files.length === 1) {
2125
- const before = await resolveManifest(opts.manifest, shells, disabled, "devdiff-before", toRoots(opts["scan-root"]));
2126
- A = before.manifest;
2127
- labelA = before.source;
2128
- B = readManifestFile(files[0]);
2129
- labelB = files[0];
2130
- } else {
2131
- const before = await resolveManifest(opts.manifest, shells, disabled, "devdiff-before", toRoots(opts["scan-root"]));
2132
- A = before.manifest;
2133
- labelA = before.source;
2134
- B = await runScan(shells, disabled, { projectRoots: toRoots(opts["scan-root"]) });
2135
- labelB = "current live scan";
2136
- }
2137
- const diff = diffManifests(A, B);
2138
- if (opts.json) {
2139
- emitText(JSON.stringify({ before: labelA, after: labelB, ...diff }, null, 2), opts.bom === true);
2140
- return 0;
2141
- }
2142
- console.log(`\x1B[1m⚖ devdiff\x1B[0m before: \x1B[2m${labelA}\x1B[0m → after: \x1B[2m${labelB}\x1B[0m`);
2143
- for (const t of diff.tools) {
2144
- for (const inst of t.added)
2145
- console.log(` \x1B[32m+ ${t.category} ${t.id}\x1B[0m ${inst.path} ${inst.version || "?"}`);
2146
- for (const inst of t.removed)
2147
- console.log(` \x1B[31m- ${t.category} ${t.id}\x1B[0m ${inst.path} ${inst.version || "?"}`);
2148
- for (const vc of t.versionChanged)
2149
- console.log(` \x1B[33m~ ${t.category} ${t.id}\x1B[0m ${vc.path} ${vc.from || "?"} → ${vc.to || "?"}`);
2150
- }
2151
- for (const iss of diff.issues) {
2152
- for (const c of iss.added)
2153
- console.log(` \x1B[31m+ issue\x1B[0m ${sevColor(c.Severity)}${c.Severity}\x1B[0m ${c.ID} ${c.Label}`);
2154
- for (const c of iss.resolved)
2155
- console.log(` \x1B[32m- issue\x1B[0m ${sevColor(c.Severity)}${c.Severity}\x1B[0m ${c.ID} ${c.Label}`);
2156
- }
2157
- const s = diff.summary;
2158
- console.log(` \x1B[2m→ +${s.installsAdded} installs / -${s.installsRemoved} installs / ~${s.versionChanges} version changes / +${s.issuesAdded} issues / -${s.issuesResolved} resolved\x1B[0m`);
2159
- if (!diff.tools.length && !diff.issues.length)
2160
- console.log(" \x1B[32mno diff\x1B[0m");
2161
- return 0;
2162
- }
2163
- function sevColor(sev) {
2164
- switch (sev) {
2165
- case "error":
2166
- return "\x1B[31m";
2167
- case "warn":
2168
- return "\x1B[33m";
2169
- case "info":
2170
- return "\x1B[36m";
2171
- case "ok":
2172
- return "\x1B[32m";
2173
- default:
2174
- return "\x1B[0m";
2175
- }
2176
- }
2177
- function renderPrettyFromManifest(m) {
2178
- const sys = m.system || {};
2179
- console.log(`\x1B[1mdevmap\x1B[0m v${m.meta?.version || VERSION} \x1B[2m${sys.hostname || "?"}\x1B[0m ${sys.platform}/${sys.arch}`);
2180
- console.log(` shell=${sys.shell || "?"} pathDirs=${sys.pathDirCount ?? "?"}`);
2181
- console.log(" " + "─".repeat(40));
2182
- for (const key of ["runtimes", "packageManagers", "ides"]) {
2183
- for (const g of m[key] || []) {
2184
- console.log(` \x1B[36m${key === "runtimes" ? "runtime" : key === "packageManagers" ? "pkg" : "ide"}\x1B[0m ${g.id} \x1B[2m(${g.installs.length})\x1B[0m`);
2185
- for (const inst of g.installs.slice(0, 6)) {
2186
- const tag = inst.onPath ? "" : inst.env ? ` \x1B[36m[env: ${inst.env}]\x1B[0m` : " \x1B[33m[off-path]\x1B[0m";
2187
- console.log(` ${inst.path} \x1B[2m${inst.version || "?"}\x1B[0m${tag}`);
2188
- }
2189
- }
2190
- }
2191
- const checks = m.checks || [];
2192
- renderEnvironments(m.environments);
2193
- if (checks.length) {
2194
- 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}`);
2198
- }
2199
- }
2200
- var plur = (n, one, many = one + "s") => `${n} ${n === 1 ? one : many}`;
2201
- function renderEnvironments(env) {
2202
- if (!env)
2203
- return;
2204
- const rule = " " + "─".repeat(40);
2205
- if (env.vcpkg?.installed) {
2206
- console.log(rule);
2207
- console.log(` \x1B[1mvcpkg\x1B[0m ${plur(env.vcpkg.packages.length, "package")} (\x1B[33m${plur(env.vcpkg.installs.length, "vcpkg copy", "vcpkg copies")}\x1B[0m)`);
2208
- for (const p of env.vcpkg.packages.slice(0, 6))
2209
- console.log(` ${p.name}@${p.version} \x1B[2m${p.triplet}\x1B[0m`);
2210
- }
2211
- if (env.pip?.perInterpreter?.length) {
2212
- console.log(rule);
2213
- console.log(` \x1B[1mpip\x1B[0m ${plur(env.pip.perInterpreter.length, "interpreter")}`);
2214
- for (const pi of env.pip.perInterpreter) {
2215
- const short = pi.pythonPath.split(/[\\/]/).slice(-2).join("/").replace(/\.exe$/i, "");
2216
- console.log(` ${short} \x1B[2m${plur(pi.packages.length, "package")}${pi.outdated.length ? " · " + pi.outdated.length + " upgradable" : ""}\x1B[0m`);
2217
- }
2218
- }
2219
- if (env.apt?.available) {
2220
- console.log(rule);
2221
- console.log(` \x1B[1mapt\x1B[0m ${plur(env.apt.count, "package")} · ${env.apt.upgradable} upgradable`);
2222
- }
2223
- if (env.condaEnvs?.envs?.length) {
2224
- console.log(rule);
2225
- console.log(` \x1B[1mconda envs\x1B[0m (${env.condaEnvs.envs.length})`);
2226
- for (const e of env.condaEnvs.envs)
2227
- console.log(` ${e.active ? "*" : " "} ${e.name} \x1B[2m${plur(e.packages.length, "package")}${e.outdated.length ? " · " + e.outdated.length + " upgradable" : ""}\x1B[0m`);
2228
- }
2229
- if (env.docker?.installed) {
2230
- console.log(rule);
2231
- console.log(` \x1B[1mdocker\x1B[0m daemon=${env.docker.daemonRunning ? "\x1B[32mrunning\x1B[0m" : "\x1B[31mnot running\x1B[0m"} images=${env.docker.images} containers=${env.docker.containers}`);
2232
- }
2233
- if (env.wsl?.available && env.wsl.distros.length) {
2234
- console.log(rule);
2235
- console.log(` \x1B[1mWSL\x1B[0m (${plur(env.wsl.distros.length, "distro")})`);
2236
- for (const d of env.wsl.distros) {
2237
- const running = /running/i.test(d.state);
2238
- const state = running ? `\x1B[32m${d.state}\x1B[0m` : `\x1B[2m${d.state}\x1B[0m`;
2239
- console.log(` ${d.default ? "*" : " "} ${d.name} ${state} \x1B[2m· WSL${d.version}\x1B[0m`);
2240
- if (d.basePath)
2241
- console.log(` \x1B[2mlocation: ${d.basePath}\x1B[0m`);
2242
- if (d.probed && d.tools) {
2243
- const have = Object.entries(d.tools).filter(([, v]) => v);
2244
- console.log(have.length ? ` \x1B[2mtools:\x1B[0m ${have.map(([k, v]) => `${k}=${v}`).join(" ")}` : ` \x1B[2mtools: (not detected)\x1B[0m`);
2245
- } else {
2246
- console.log(` \x1B[2mtools: not probed (distro not running; add --wsl-deep to probe)\x1B[0m`);
2247
- }
2248
- }
2249
- }
2250
- if (env.vsShells?.shells?.length) {
2251
- console.log(rule);
2252
- console.log(` \x1B[1mVisual Studio dev shells\x1B[0m (${env.vsShells.shells.length})`);
2253
- for (const s of env.vsShells.shells) {
2254
- const present = Object.entries(s.tools).filter(([, v]) => v).map(([k]) => k);
2255
- const tag = present.length ? `\x1B[32m${present.join("/")}\x1B[0m` : `\x1B[33mno C++ toolchain\x1B[0m`;
2256
- console.log(` ${s.name} \x1B[2m[${s.kind}/${s.arch}]\x1B[0m ${tag}`);
2257
- }
2258
- }
2259
- if (env.projectEnvs?.envs?.length) {
2260
- console.log(rule);
2261
- console.log(` \x1B[1mproject-local environments\x1B[0m (${env.projectEnvs.envs.length})`);
2262
- for (const e of env.projectEnvs.envs.slice(0, 12))
2263
- console.log(` ${e.project} \x1B[2m${e.type}\x1B[0m ${e.path}`);
2264
- }
2265
- }
2266
- function renderMarkdownLite(m) {
2267
- const lines = [`# devmap report`, "", `> host: ${m.system?.hostname} · ${m.system?.platform}/${m.system?.arch} · pathDirs=${m.system?.pathDirCount}`, ""];
2268
- for (const key of ["runtimes", "packageManagers", "ides"]) {
2269
- lines.push(`## ${key}`);
2270
- for (const g of m[key] || []) {
2271
- lines.push(`- **${g.id}** (${g.installs.length})`);
2272
- for (const inst of g.installs) {
2273
- const tag = locationTag(inst);
2274
- lines.push(` - ${inst.path} — ${inst.version || "?"}${tag ? ` (${tag})` : ""}`);
2275
- }
2276
- }
2277
- lines.push("");
2278
- }
2279
- if ((m.checks || []).length) {
2280
- lines.push(`## issues (${m.checks.length})`);
2281
- for (const c of m.checks)
2282
- lines.push(`- [${c.Severity}] \`${c.ID}\` ${c.Label} — ${c.Detail}`);
2283
- }
2284
- const env = m.environments || {};
2285
- const envLines = [];
2286
- if (env.vcpkg?.installed)
2287
- envLines.push(`- vcpkg: ${plur(env.vcpkg.packages.length, "package")} / ${plur(env.vcpkg.installs.length, "vcpkg copy", "vcpkg copies")}`);
2288
- if (env.pip?.perInterpreter?.length)
2289
- envLines.push(`- pip: ${plur(env.pip.perInterpreter.length, "interpreter")} / ${plur(env.pip.perInterpreter.reduce((a, p) => a + p.packages.length, 0), "package")}`);
2290
- if (env.apt?.available)
2291
- envLines.push(`- apt: ${plur(env.apt.count, "package")} / ${env.apt.upgradable} upgradable`);
2292
- if (env.condaEnvs?.envs?.length)
2293
- envLines.push(`- conda envs: ${env.condaEnvs.envs.length}`);
2294
- if (env.docker?.installed)
2295
- envLines.push(`- docker: daemon=${env.docker.daemonRunning} images=${env.docker.images} containers=${env.docker.containers}`);
2296
- if (env.wsl?.available)
2297
- envLines.push(`- WSL: ${plur(env.wsl.distros?.length || 0, "distro")}`);
2298
- if (env.vsShells?.shells?.length)
2299
- envLines.push(`- Visual Studio dev shells: ${env.vsShells.shells.length}`);
2300
- if (env.projectEnvs?.envs?.length)
2301
- envLines.push(`- project-local environments: ${env.projectEnvs.envs.length}`);
2302
- if (envLines.length) {
2303
- lines.push("", `## environments (Phase 2)`);
2304
- lines.push(...envLines);
2305
- }
2306
- return lines.join(`
2307
- `);
2308
- }
2309
- function printHelp() {
2310
- console.log(`devmap v${VERSION} — machine dev-environment source of truth
2311
-
2312
- Usage:
2313
- devmap [scan] [options] scan and render nicely (scan is the DEFAULT — 'devmap' alone is enough)
2314
- devmap query <tool> query a generated manifest (devmap.json)
2315
- devmap devdiff [a] [b] compare two manifests (environment drift / cross-machine consistency)
2316
-
2317
- Tip: every 'devmap scan ...' also works as just 'devmap ...' (the 'scan' word is optional).
2318
-
2319
- scan options:
2320
- --json emit machine-readable JSON (devmap.json schema)
2321
- --report emit full markdown report
2322
- --no-manifest do not persist the manifest to disk
2323
- --manifest <p> specify the manifest location (default ~/.devmap/devmap.json)
2324
- --disable <r1,r2> disable check rules (prefix match, e.g. path.duplicate / pip.outdated)
2325
- --shell <list> which shell views to reconstruct (current|powershell|gitbash|wsl|all)
2326
- --scan-root <dir> project-local environment scan root (comma-separated; default cwd). e.g. --scan-root C:\\src
2327
- --outdated check pip/conda upgradable packages online (off by default: requires PyPI access and slows the scan)
2328
- --wsl-deep probe inside stopped WSL distros (default probes only running ones to avoid waking distros)
2329
- --ci CI gate: clean → exit 0; any warn → exit 2; any error → exit 1
2330
- --audit-deps dependency CVE audit via OSV-Scanner/pip-audit/govulncheck/Trivy (opt-in, uses network)
2331
- --health-network run network health probes too (npm doctor / rustup check; off by default to stay offline-fast)
2332
- --quiet suppress scan progress feedback (for scripts/CI)
2333
- --bom prepend UTF-8 BOM for viewing in Notepad etc. (only on redirect/pipe)
2334
- --version / --help
2335
-
2336
- Phase 2 environment layer (covered by this scan):
2337
- vcpkg pkgs / per-python-interpreter pip packages (incl. upgradable) / apt packages (Linux) / conda env packages /
2338
- docker (binary+daemon+images/containers) / WSL distros / Visual Studio dev shells / project-local environments (venv, node_modules)
2339
-
2340
- Encoding note: output is plain UTF-8. Opening BOM-less UTF-8 in Windows Notepad may show mojibake,
2341
- use VS Code / Windows Terminal cat / an IDE to open it; or add --bom then redirect.
2342
- When reading a BOM file in Python use open(p, encoding="utf-8-sig").
2343
-
2344
- query options:
2345
- <tool> tool id substring (e.g. node), or "category.tool" dotted form (e.g. runtime.node / pkg.npm)
2346
- --issues list issues only
2347
- --conflicts list conflicts only (multiVersion/path/ghost)
2348
- --off-path list isolated off-PATH installs only (version-manager envs excluded)
2349
- --kind <runtime|packageManager|ide> filter by category
2350
- --where <substr> filter by install-path substring
2351
- --manifest <p> manifest to query (default ~/.devmap/devmap.json; live scan if absent)
2352
- --json emit JSON
2353
-
2354
- devdiff options:
2355
- (no args) compare "stored manifest" vs "current live scan"
2356
- <file> compare "stored/specified before" vs <file>
2357
- <a> <b> compare manifests a and b
2358
- --json emit JSON
2359
- `);
2360
- }
2361
- async function main() {
2362
- const { pos, opts } = tokenize(process.argv.slice(2));
2363
- if (opts.help || opts.h) {
2364
- printHelp();
2365
- process.exit(0);
2366
- }
2367
- if (opts.version || opts.v) {
2368
- console.log(VERSION);
2369
- process.exit(0);
2370
- }
2371
- const sub = pos[0] || "scan";
2372
- let code = 0;
2373
- try {
2374
- if (sub === "query")
2375
- code = await cmdQuery(opts, pos);
2376
- else if (sub === "devdiff")
2377
- code = await cmdDevdiff(opts, pos);
2378
- else
2379
- code = await cmdScan(opts, pos);
2380
- } catch (e) {
2381
- console.error(`\x1B[31merror:\x1B[0m ${e.message}`);
2382
- process.exit(2);
2383
- }
2384
- process.exit(code);
2385
- }
2386
- main();
1
+ #!/usr/bin/env node
2
+ // @bun
3
+
4
+ // src/version.ts
5
+ var VERSION = "0.3.8";
6
+
7
+ // src/install.ts
8
+ function isIsolated(i) {
9
+ return !i.onPath && !i.env;
10
+ }
11
+ function locationOf(i) {
12
+ if (i.onPath)
13
+ return "PATH";
14
+ if (i.env)
15
+ return "env";
16
+ return "off-path";
17
+ }
18
+ function locationTag(i) {
19
+ if (i.onPath)
20
+ return "";
21
+ return i.env ? `env: ${i.env}` : "off-path";
22
+ }
23
+
24
+ // src/detector/registry.ts
25
+ var registry = [];
26
+ function register(d) {
27
+ registry.push(d);
28
+ return d;
29
+ }
30
+ async function runDetectors(ctx, progress) {
31
+ const byName = {};
32
+ const allChecks = [];
33
+ progress?.setTotal(registry.length);
34
+ await Promise.all(registry.map(async (d) => {
35
+ progress?.start(d.name);
36
+ const t0 = Date.now();
37
+ try {
38
+ const r = await d.scan(ctx);
39
+ byName[d.name] = r.data ?? {};
40
+ if (r.checks && r.checks.length)
41
+ allChecks.push(...r.checks);
42
+ progress?.done(d.name, Date.now() - t0, false);
43
+ } catch (e) {
44
+ progress?.done(d.name, Date.now() - t0, true);
45
+ byName[d.name] = { error: String(e?.message || e) };
46
+ }
47
+ }));
48
+ return { byName, allChecks };
49
+ }
50
+
51
+ // src/check/model.ts
52
+ function makeCheck(id, kind, severity, label, detail, path = [], fix) {
53
+ const c = { ID: id, Kind: kind, Severity: severity, Label: label, Detail: detail, Path: path };
54
+ if (fix)
55
+ c.Fix = fix;
56
+ return c;
57
+ }
58
+
59
+ // src/platform/exec.ts
60
+ import { spawnSync, spawn } from "node:child_process";
61
+ import { readdirSync, statSync, lstatSync, existsSync } from "node:fs";
62
+ import { join, basename } from "node:path";
63
+ var IS_WIN = process.platform === "win32";
64
+ var PATH_SEP = IS_WIN ? ";" : ":";
65
+ var PATHEXT = (process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM").split(";").map((s) => s.toLowerCase());
66
+ function decodeOutput(buf) {
67
+ if (buf.length === 0)
68
+ return "";
69
+ if (buf.length >= 2 && buf[0] === 255 && buf[1] === 254)
70
+ return buf.subarray(2).toString("utf16le");
71
+ if (buf.length >= 3 && buf[0] === 239 && buf[1] === 187 && buf[2] === 191)
72
+ return buf.subarray(3).toString("utf8");
73
+ const sample = Math.min(buf.length, 512);
74
+ let odd = 0;
75
+ let nulOdd = 0;
76
+ for (let i = 1;i < sample; i += 2) {
77
+ odd++;
78
+ if (buf[i] === 0)
79
+ nulOdd++;
80
+ }
81
+ if (odd >= 4 && nulOdd / odd > 0.5)
82
+ return buf.toString("utf16le");
83
+ return buf.toString("utf8");
84
+ }
85
+ function run(cmd, args = [], timeout = 5000) {
86
+ try {
87
+ const r = spawnSync(cmd, args, { encoding: "buffer", timeout, windowsHide: true });
88
+ if (r.error)
89
+ return "";
90
+ const buf = Buffer.concat([r.stdout || Buffer.alloc(0), r.stderr || Buffer.alloc(0)]);
91
+ return decodeOutput(buf).trim();
92
+ } catch {
93
+ return "";
94
+ }
95
+ }
96
+ var MAX_PROC = 16;
97
+ var activeProcs = 0;
98
+ var procWaiters = [];
99
+ function acquireProc() {
100
+ if (activeProcs < MAX_PROC) {
101
+ activeProcs++;
102
+ return Promise.resolve();
103
+ }
104
+ return new Promise((resolve) => procWaiters.push(resolve)).then(() => {
105
+ activeProcs++;
106
+ });
107
+ }
108
+ function releaseProc() {
109
+ activeProcs--;
110
+ const next = procWaiters.shift();
111
+ if (next)
112
+ next();
113
+ }
114
+ async function runAsync(cmd, args = [], timeout = 5000) {
115
+ await acquireProc();
116
+ try {
117
+ return await spawnCollect(cmd, args, timeout);
118
+ } finally {
119
+ releaseProc();
120
+ }
121
+ }
122
+ function spawnCollect(cmd, args, timeout) {
123
+ return new Promise((resolve) => {
124
+ let settled = false;
125
+ const finish = (s) => {
126
+ if (settled)
127
+ return;
128
+ settled = true;
129
+ resolve(s);
130
+ };
131
+ let child;
132
+ try {
133
+ child = spawn(cmd, args, { windowsHide: true });
134
+ } catch {
135
+ return finish("");
136
+ }
137
+ const chunks = [];
138
+ child.stdout?.on("data", (d) => chunks.push(Buffer.from(d)));
139
+ child.stderr?.on("data", (d) => chunks.push(Buffer.from(d)));
140
+ child.stdout?.on("error", () => {});
141
+ child.stderr?.on("error", () => {});
142
+ const timer = setTimeout(() => {
143
+ try {
144
+ child.kill();
145
+ } catch {}
146
+ finish("");
147
+ }, timeout);
148
+ child.on("error", () => {
149
+ clearTimeout(timer);
150
+ finish("");
151
+ });
152
+ child.on("close", () => {
153
+ clearTimeout(timer);
154
+ finish(decodeOutput(Buffer.concat(chunks)).trim());
155
+ });
156
+ });
157
+ }
158
+ async function pool(items, limit, fn) {
159
+ const results = new Array(items.length);
160
+ let cursor = 0;
161
+ const workers = new Array(Math.max(1, Math.min(limit, items.length))).fill(0).map(async () => {
162
+ for (;; ) {
163
+ const i = cursor++;
164
+ if (i >= items.length)
165
+ return;
166
+ results[i] = await fn(items[i], i);
167
+ }
168
+ });
169
+ await Promise.all(workers);
170
+ return results;
171
+ }
172
+ function wrapBin(binPath, args) {
173
+ if (IS_WIN && /\.(cmd|bat)$/i.test(binPath))
174
+ return ["cmd", ["/c", binPath, ...args]];
175
+ if (IS_WIN && /\.ps1$/i.test(binPath)) {
176
+ const quoted = args.map((a) => `'${a.replace(/'/g, "''")}'`).join(" ");
177
+ return ["powershell", ["-NoProfile", "-Command", `& '${binPath}' ${quoted}`]];
178
+ }
179
+ return [binPath, args];
180
+ }
181
+ function runBinAsync(binPath, args = [], timeout = 5000) {
182
+ const [c, a] = wrapBin(binPath, args);
183
+ return runAsync(c, a, timeout);
184
+ }
185
+ function pathDirs() {
186
+ return (process.env.PATH || "").split(PATH_SEP).map((s) => s.trim()).filter(Boolean);
187
+ }
188
+ function hasExecExt(name) {
189
+ if (!IS_WIN)
190
+ return true;
191
+ const lower = name.toLowerCase();
192
+ return PATHEXT.some((ext) => lower.endsWith(ext));
193
+ }
194
+ var dirExecCache = new Map;
195
+ function execEntriesOf(dir) {
196
+ const key = IS_WIN ? dir.toLowerCase() : dir;
197
+ const hit = dirExecCache.get(key);
198
+ if (hit)
199
+ return hit;
200
+ const out = [];
201
+ let entries = [];
202
+ try {
203
+ entries = readdirSync(dir);
204
+ } catch {
205
+ dirExecCache.set(key, out);
206
+ return out;
207
+ }
208
+ for (const e of entries) {
209
+ if (IS_WIN && !hasExecExt(e))
210
+ continue;
211
+ const full = join(dir, e);
212
+ let st;
213
+ try {
214
+ st = statSync(full);
215
+ } catch {
216
+ continue;
217
+ }
218
+ if (!st.isFile())
219
+ continue;
220
+ if (!IS_WIN && !(st.mode & 73))
221
+ continue;
222
+ out.push({ name: IS_WIN ? e.toLowerCase() : e, full });
223
+ }
224
+ dirExecCache.set(key, out);
225
+ return out;
226
+ }
227
+ function whichInDirs(name, dirs) {
228
+ const out = [];
229
+ const target = IS_WIN ? name.toLowerCase() : name;
230
+ for (const dir of dirs) {
231
+ for (const e of execEntriesOf(dir)) {
232
+ if (IS_WIN) {
233
+ if (e.name === target || PATHEXT.some((ext) => e.name === target + ext))
234
+ out.push(e.full);
235
+ } else if (e.name === target) {
236
+ out.push(e.full);
237
+ }
238
+ }
239
+ }
240
+ return out;
241
+ }
242
+ var SEMVER_RE = /(?<![\d.])(\d+\.\d+\.\d+)(?:[-.+][0-9A-Za-z._-]+)*(?![\d.])/;
243
+ var LOOSE_VERSION_RE = /(?<![\d.])(\d+\.\d+)(?:[-.+][0-9A-Za-z._-]+)*(?![\d.])/;
244
+ function extractVersion(out, binPath) {
245
+ const lines = out.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("["));
246
+ if (binPath) {
247
+ const self = basename(binPath).replace(/\.(exe|cmd|bat|ps1|sh)$/i, "").toLowerCase();
248
+ const own = lines.find((l) => {
249
+ const key = l.toLowerCase().replace(/^['"]/, "");
250
+ return key.startsWith(self + ":") || key.startsWith(self + "':") || key.startsWith(self + '":');
251
+ });
252
+ if (own) {
253
+ const m = own.match(SEMVER_RE) || own.match(LOOSE_VERSION_RE);
254
+ if (m)
255
+ return m[0];
256
+ }
257
+ }
258
+ for (const l of lines) {
259
+ const m = l.match(SEMVER_RE);
260
+ if (m)
261
+ return m[0];
262
+ }
263
+ for (const l of lines) {
264
+ const m = l.match(LOOSE_VERSION_RE);
265
+ if (m)
266
+ return m[0];
267
+ }
268
+ return null;
269
+ }
270
+ async function versionOfAsync(binPath, flags, timeout = 9000) {
271
+ for (const f of flags) {
272
+ const out = await runBinAsync(binPath, [f], timeout);
273
+ if (out) {
274
+ const v = extractVersion(out, binPath);
275
+ if (v)
276
+ return v;
277
+ }
278
+ }
279
+ return null;
280
+ }
281
+ function fileExists(p) {
282
+ try {
283
+ return existsSync(p);
284
+ } catch {
285
+ return false;
286
+ }
287
+ }
288
+ function scanBrokenSymlinks(dirs) {
289
+ const ghosts = [];
290
+ for (const dir of dirs) {
291
+ if (/[\\/]microsoft[\\/]windowsapps[\\/]?$/i.test(dir))
292
+ continue;
293
+ let entries = [];
294
+ try {
295
+ entries = readdirSync(dir);
296
+ } catch {
297
+ continue;
298
+ }
299
+ for (const e of entries) {
300
+ const full = join(dir, e);
301
+ let lstat;
302
+ try {
303
+ lstat = lstatSync(full);
304
+ } catch {
305
+ continue;
306
+ }
307
+ if (lstat.isSymbolicLink()) {
308
+ try {
309
+ statSync(full);
310
+ } catch (err) {
311
+ const code = err?.code;
312
+ if (code === "ENOENT") {
313
+ ghosts.push({ path: full, reason: "broken-symlink (target missing)" });
314
+ }
315
+ }
316
+ }
317
+ }
318
+ }
319
+ return ghosts;
320
+ }
321
+
322
+ // src/platform/shells.ts
323
+ function shellName(s) {
324
+ return { current: "current", powershell: "PowerShell", gitbash: "Git Bash", wsl: "WSL" }[s];
325
+ }
326
+ function shellPathDirs(shell) {
327
+ if (shell === "current" || !IS_WIN)
328
+ return pathDirs();
329
+ try {
330
+ if (shell === "powershell") {
331
+ const out = run("powershell", [
332
+ "-NoProfile",
333
+ "-Command",
334
+ "$p=[Environment]::GetEnvironmentVariable('PATH','Process');$u=[Environment]::GetEnvironmentVariable('PATH','User');$m=[Environment]::GetEnvironmentVariable('PATH','Machine'); ($p,$u,$m) -join ';'"
335
+ ]);
336
+ return (out || "").split(";").map((s) => s.trim()).filter(Boolean);
337
+ }
338
+ if (shell === "gitbash") {
339
+ const out = run("bash", ["-lc", 'echo "$PATH"']);
340
+ return (out || "").split(":").map((s) => s.trim()).filter(Boolean);
341
+ }
342
+ if (shell === "wsl") {
343
+ const out = run("wsl", ["bash", "-lc", 'echo "$PATH"']);
344
+ return (out || "").split(":").map((s) => s.trim()).filter(Boolean);
345
+ }
346
+ } catch {
347
+ return pathDirs();
348
+ }
349
+ return pathDirs();
350
+ }
351
+
352
+ // src/detector/conflicts.ts
353
+ function computeConflicts(groups, shells, disabled) {
354
+ const checks = [];
355
+ const multiVersionTools = [];
356
+ const pathConflicts = [];
357
+ const offPathNotes = [];
358
+ const multiShell = shells.length > 1;
359
+ for (const g of groups) {
360
+ const distinctVersions = new Set(g.installs.map((i) => i.version || ""));
361
+ if (g.installs.length > 1 && distinctVersions.size > 1) {
362
+ multiVersionTools.push({ id: g.id, count: g.installs.length, versions: g.installs.map((i) => i.version) });
363
+ checks.push(makeCheck(`version.multi.${g.id}`, "conflict", "warn", `${g.id} has ${g.installs.length} installs (${distinctVersions.size} distinct versions)`, `Versions: ${g.installs.map((i) => i.version || "?").join(", ")} (multiple versions installed, check which one is active)`, g.installs.map((i) => i.path)));
364
+ }
365
+ if (multiShell) {
366
+ const res = {};
367
+ for (const s of shells)
368
+ res[s] = (whichInDirs(g.id, shellPathDirs(s)) || [])[0] || null;
369
+ g.shellResolution = res;
370
+ const vals = Object.values(res).filter(Boolean);
371
+ if (new Set(vals).size > 1) {
372
+ checks.push(makeCheck(`shell.mismatch.${g.id}`, "mismatch", "info", `${g.id} resolves differently across shells`, shells.map((s) => `${shellName(s)} → ${res[s] || "(not found)"}`).join(" | "), vals));
373
+ }
374
+ }
375
+ const off = g.installs.filter(isIsolated);
376
+ if (off.length) {
377
+ const dedupe = Array.from(new Set(off.map((i) => i.path)));
378
+ offPathNotes.push(...off.map((i) => ({ id: g.id, path: i.path })));
379
+ checks.push(makeCheck(`path.off.${g.id}`, "mismatch", "info", `${g.id} is installed but not on the current shell's PATH`, `Not a version-manager environment, so nothing will activate it for you; add it to PATH manually to use it: ${dedupe.join(", ")}`, dedupe));
380
+ }
381
+ }
382
+ for (const g of groups) {
383
+ const seen = new Set;
384
+ const unique = [];
385
+ for (const i of g.installs) {
386
+ if (!i.onPath)
387
+ continue;
388
+ const k = i.path.toLowerCase();
389
+ if (seen.has(k))
390
+ continue;
391
+ seen.add(k);
392
+ unique.push({ path: i.path, version: i.version || null });
393
+ }
394
+ if (unique.length > 1) {
395
+ const versions = new Set(unique.map((u) => u.version || ""));
396
+ const sameVersion = versions.size === 1;
397
+ const resolvedTo = unique[0].path;
398
+ const alsoAt = unique.slice(1).map((u) => u.path);
399
+ if (!sameVersion) {
400
+ pathConflicts.push({ name: g.id, resolvedTo, alsoAt });
401
+ }
402
+ checks.push(makeCheck(`path.duplicate.${g.id}`, "conflict", sameVersion ? "info" : "warn", sameVersion ? `${g.id} has ${unique.length} launcher entries on PATH (all ${unique[0].version || "same version"}; no version conflict)` : `${g.id}: version ${unique[0].version} shadows ${[...versions].slice(1).join(", ")} on PATH`, sameVersion ? `active → ${resolvedTo}; also at: ${alsoAt.join(", ")} — identical version, harmless redundancy` : `active → ${resolvedTo}; shadowed (different version): ${alsoAt.join(", ")}`, unique.map((u) => u.path)));
403
+ }
404
+ }
405
+ const seenDir = new Set;
406
+ const uniqueDirs = [];
407
+ for (const d of pathDirs()) {
408
+ const k = d.toLowerCase();
409
+ if (seenDir.has(k))
410
+ continue;
411
+ seenDir.add(k);
412
+ uniqueDirs.push(d);
413
+ }
414
+ const ghosts = scanBrokenSymlinks(uniqueDirs);
415
+ const dirFreq = new Map;
416
+ for (const d of pathDirs()) {
417
+ const k = d.toLowerCase();
418
+ dirFreq.set(k, (dirFreq.get(k) || 0) + 1);
419
+ }
420
+ const dupDirs = [...dirFreq.entries()].filter(([, c]) => c > 1).map(([d, c]) => ({ d, c }));
421
+ if (dupDirs.length) {
422
+ const redundant = dupDirs.reduce((s, x) => s + x.c - 1, 0);
423
+ checks.push(makeCheck("path.duplicate-dir", "conflict", "info", `PATH contains ${dupDirs.length} duplicate directories (${redundant} redundant entries, amplifies shadowing)`, `Duplicates: ${dupDirs.map((x) => `${x.d} ×${x.c}`).join("; ")}`, dupDirs.map((x) => x.d)));
424
+ }
425
+ for (const gh of ghosts) {
426
+ checks.push(makeCheck(`ghost.broken.${gh.path}`, "ghost", "warn", `Broken symlink found (ghost binary)`, `${gh.path} -- ${gh.reason}`, [gh.path]));
427
+ }
428
+ const filtered = disabled && disabled.size ? checks.filter((c) => !isDisabled(c.ID, disabled)) : checks;
429
+ return { checks: filtered, multiVersionTools, pathConflicts, ghostBinaries: ghosts, offPathNotes };
430
+ }
431
+ function isDisabled(id, disabled) {
432
+ for (const d of disabled)
433
+ if (id === d || id.startsWith(d + ".") || id.startsWith(d))
434
+ return true;
435
+ return false;
436
+ }
437
+ function isCheckDisabled(id, disabled) {
438
+ if (!disabled || !disabled.size)
439
+ return false;
440
+ return isDisabled(id, disabled);
441
+ }
442
+
443
+ // src/security/runtimeAdvisories.ts
444
+ var RUNTIME_ADVISORIES = {
445
+ node: {
446
+ eolByVersion: {
447
+ "16": "2023-09-11",
448
+ "18": "2025-04-30",
449
+ "20": "2026-04-30",
450
+ "22": "2027-04-30",
451
+ "24": "2028-04-30",
452
+ "26": "2029-04-30"
453
+ }
454
+ },
455
+ python: {
456
+ eolByVersion: {
457
+ "3.7": "2023-06-27",
458
+ "3.8": "2024-10-07",
459
+ "3.9": "2025-10-31",
460
+ "3.10": "2026-10-31",
461
+ "3.11": "2027-10-31",
462
+ "3.12": "2028-10-31",
463
+ "3.13": "2029-10-31"
464
+ }
465
+ },
466
+ go: {
467
+ eolByVersion: {
468
+ "1.20": "2024-02-06",
469
+ "1.21": "2024-08-13",
470
+ "1.22": "2025-02-11",
471
+ "1.23": "2025-08-12",
472
+ "1.24": "2026-02-11"
473
+ }
474
+ }
475
+ };
476
+
477
+ // src/security/scan.ts
478
+ function eolKeys(version) {
479
+ const parts = version.split(".");
480
+ const maj = parts[0];
481
+ const min = parts[1] ?? "0";
482
+ return [maj, `${maj}.${min}`];
483
+ }
484
+ function computeSecurity(byName) {
485
+ const checks = [];
486
+ const runtimes = byName.runtimes || [];
487
+ const now = Date.now();
488
+ for (const g of runtimes) {
489
+ const adv = RUNTIME_ADVISORIES[g.id];
490
+ if (!adv)
491
+ continue;
492
+ for (const inst of g.installs) {
493
+ const v = inst.version;
494
+ if (!v)
495
+ continue;
496
+ for (const key of eolKeys(v)) {
497
+ const eol = adv.eolByVersion[key];
498
+ if (eol) {
499
+ const eolT = Date.parse(eol + "T00:00:00Z");
500
+ if (!isNaN(eolT) && eolT < now) {
501
+ checks.push(makeCheck(`security.eol.${g.id}`, "stale", "warn", `${g.id} ${v} is end-of-life (since ${eol})`, `${g.id} ${v} reached end-of-life on ${eol} and no longer receives security patches. Upgrade to a supported release. (${inst.path})`, [inst.path]));
502
+ }
503
+ break;
504
+ }
505
+ }
506
+ }
507
+ }
508
+ return checks;
509
+ }
510
+
511
+ // src/security/sca.ts
512
+ import { spawnSync as spawnSync2 } from "child_process";
513
+ function whichTool(name) {
514
+ const isWin = process.platform === "win32";
515
+ const r = spawnSync2(isWin ? "where" : "command", isWin ? [name] : ["-v", name], { encoding: "utf-8" });
516
+ if (r.error)
517
+ return null;
518
+ const out = (r.stdout || "").split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
519
+ return out || null;
520
+ }
521
+ function getVersion(toolId) {
522
+ const r = spawnSync2(toolId, ["--version"], { encoding: "utf-8", timeout: 15000 });
523
+ if (r.error || !r.stdout)
524
+ return null;
525
+ const m = r.stdout.match(/(\d+\.\d+\.\d+)/);
526
+ return m ? m[1] : null;
527
+ }
528
+ function toSeverity(s) {
529
+ const t = (s || "").toUpperCase();
530
+ if (t === "CRITICAL" || t === "HIGH")
531
+ return "error";
532
+ return "warn";
533
+ }
534
+ var SCA_TOOLS = [
535
+ {
536
+ id: "osv-scanner",
537
+ label: "OSV-Scanner",
538
+ auditArgs: ["--format", "json", "."],
539
+ parse: (json) => {
540
+ const out = [];
541
+ for (const res of json?.results || []) {
542
+ for (const v of res?.vulnerabilities || []) {
543
+ const pkg = v?.package || {};
544
+ const sev = toSeverity(v?.severity || v?.database_specific?.severity);
545
+ const vid = v?.id || "UNKNOWN";
546
+ out.push(makeCheck(`security.deps.osv.${vid}`, "vuln", sev, `${pkg.name || "?"}@${pkg.version || "?"} affected by ${vid} (${sev === "error" ? "HIGH" : "MEDIUM"})`, `${vid}: ${v?.summary || v?.details || ""}. Package ${pkg.name}@${pkg.version}.`, [pkg.name || ""]));
547
+ }
548
+ }
549
+ return out;
550
+ }
551
+ },
552
+ {
553
+ id: "pip-audit",
554
+ label: "pip-audit",
555
+ auditArgs: ["-f", "json"],
556
+ parse: (json) => {
557
+ const out = [];
558
+ const sevOf = (s) => s ? toSeverity(s) : "error";
559
+ const seen = new Set;
560
+ const push = (name, vid, sev, title, detail) => {
561
+ const key = `${name}|${vid}`;
562
+ if (seen.has(key))
563
+ return;
564
+ seen.add(key);
565
+ out.push(makeCheck(`security.deps.pipaudit.${vid}`, "vuln", sev, title, detail, [name]));
566
+ };
567
+ for (const dep of json?.dependencies || []) {
568
+ for (const v of dep?.vulns || []) {
569
+ const sev = sevOf(v?.severity);
570
+ const vid = v?.id || "UNKNOWN";
571
+ push(dep?.name || "?", vid, sev, `${dep?.name || "?"}@${dep?.version || "?"} affected by ${vid} (${sev === "error" ? "HIGH" : "MEDIUM"})`, `${vid}: ${v?.description || ""}. Fix versions: ${(v?.fix_versions || []).join(", ") || "n/a"}.`);
572
+ }
573
+ }
574
+ for (const v of json?.vulnerabilities || []) {
575
+ const sev = sevOf(v?.severity);
576
+ const vid = v?.id || "UNKNOWN";
577
+ push(v?.name || "?", vid, sev, `${v?.name || "?"} affected by ${vid}`, `${vid}: ${v?.description || ""}.`);
578
+ }
579
+ return out;
580
+ }
581
+ },
582
+ {
583
+ id: "govulncheck",
584
+ label: "govulncheck",
585
+ auditArgs: ["-json", "."],
586
+ parse: (json) => {
587
+ const out = [];
588
+ for (const res of json?.Results || []) {
589
+ for (const v of res?.Vulnerabilities || []) {
590
+ const score = v?.Severity?.Score;
591
+ const sev = typeof score === "number" && score >= 9 ? "error" : "warn";
592
+ const vid = v?.OSV?.id || "UNKNOWN";
593
+ const pkg = v?.Package?.Path || v?.Package?.Name || "?";
594
+ out.push(makeCheck(`security.deps.govulncheck.${vid}`, "vuln", sev, `${pkg}@${v?.Version || "?"} affected by ${vid} (${sev === "error" ? "HIGH" : "MEDIUM"})`, `${vid}: ${v?.Details || ""}. Package ${pkg}@${v?.Version || "?"}.`, [pkg]));
595
+ }
596
+ }
597
+ return out;
598
+ }
599
+ },
600
+ {
601
+ id: "trivy",
602
+ label: "Trivy",
603
+ auditArgs: ["fs", "--format", "json", "."],
604
+ parse: (json) => {
605
+ const out = [];
606
+ for (const res of json?.Results || []) {
607
+ for (const v of res?.Vulnerabilities || []) {
608
+ const sev = toSeverity(v?.Severity);
609
+ const vid = v?.VulnerabilityID || "UNKNOWN";
610
+ out.push(makeCheck(`security.deps.trivy.${vid}`, "vuln", sev, `${v?.PkgName || "?"}@${v?.InstalledVersion || "?"} affected by ${vid} (${sev === "error" ? "HIGH" : "MEDIUM"})`, `${vid}: ${v?.Title || v?.Description || ""}. Fixed in ${v?.FixedVersion || "n/a"}.`, [v?.PkgName || ""]));
611
+ }
612
+ }
613
+ return out;
614
+ }
615
+ }
616
+ ];
617
+ function detectScaTools() {
618
+ return SCA_TOOLS.map((t) => {
619
+ const p = whichTool(t.id);
620
+ return { id: t.id, label: t.label, path: p, version: p ? getVersion(t.id) : null };
621
+ });
622
+ }
623
+ function computeScaInventory() {
624
+ return detectScaTools().map((d) => {
625
+ const id = `security.scanner.${d.id}`;
626
+ if (d.path) {
627
+ return makeCheck(id, "vuln", "info", `${d.label} available (${d.version || "?"})`, `${d.label} is installed at ${d.path}. devmap can shell out to it for dependency CVE audit with --audit-deps.`, [d.path || ""]);
628
+ }
629
+ return makeCheck(id, "vuln", "info", `${d.label} not installed`, `${d.label} not found on PATH. Install it to enable dependency CVE scanning via devmap --audit-deps (it fetches vuln DBs over the network).`, []);
630
+ });
631
+ }
632
+ function runScaAudit(cwd) {
633
+ const present = detectScaTools().filter((d) => d.path);
634
+ if (present.length === 0) {
635
+ return [
636
+ makeCheck("security.deps.none", "vuln", "info", "no SCA tool installed — dependency CVE audit skipped", "Install osv-scanner / pip-audit / govulncheck / trivy to enable dependency CVE scanning via `devmap --audit-deps`.", [])
637
+ ];
638
+ }
639
+ const checks = [];
640
+ for (const d of present) {
641
+ const tool = SCA_TOOLS.find((t) => t.id === d.id);
642
+ const r = spawnSync2(d.id, tool.auditArgs, { cwd, encoding: "utf-8", timeout: 120000, maxBuffer: 16 * 1024 * 1024 });
643
+ if (r.error || r.status === null) {
644
+ checks.push(makeCheck(`security.deps.${d.id}.error`, "vuln", "info", `${d.label} audit failed to start`, `${d.label} could not be launched: ${r.error?.message || "unknown error"}.`, []));
645
+ continue;
646
+ }
647
+ if (r.status !== 0 && !r.stdout.trim()) {
648
+ checks.push(makeCheck(`security.deps.${d.id}.skip`, "vuln", "info", `${d.label} audit produced no output (exit ${r.status})`, `${d.label} returned no scannable result in ${cwd} (no project / no lockfile / no module).`, []));
649
+ continue;
650
+ }
651
+ try {
652
+ const json = JSON.parse(r.stdout);
653
+ const parsed = tool.parse(json, d.label);
654
+ checks.push(...parsed);
655
+ if (parsed.length === 0) {
656
+ checks.push(makeCheck(`security.deps.${d.id}.clean`, "vuln", "info", `${d.label} audit: no vulnerabilities found`, `${d.label} found no known vulnerabilities in ${cwd}.`, []));
657
+ }
658
+ } catch (e) {
659
+ checks.push(makeCheck(`security.deps.${d.id}.parse`, "vuln", "info", `${d.label} audit output not JSON`, `${d.label} returned output that could not be parsed as JSON: ${e?.message || e}.`, []));
660
+ }
661
+ }
662
+ return checks;
663
+ }
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 tally = real.map((p) => `${p.name}: ${p.items}`).join(`
752
+ `);
753
+ const raw = t.length > 4000 ? t.slice(0, 4000) + `
754
+ ... (truncated)` : t;
755
+ return {
756
+ status: "warn",
757
+ summary: `conda doctor: ${real.length} check${real.length === 1 ? "" : "s"} failed (${total} affected files)`,
758
+ detail: `${tally}
759
+
760
+ ${raw}`
761
+ };
762
+ }
763
+
764
+ // src/health/scan.ts
765
+ var NETWORK_PROBES = [
766
+ {
767
+ id: "npm",
768
+ label: "npm",
769
+ kind: "mismatch",
770
+ network: true,
771
+ candidates: [["npm", ["doctor"]]],
772
+ timeout: 20000,
773
+ parse: parseNpmDoctor
774
+ },
775
+ {
776
+ id: "rustup",
777
+ label: "rustup",
778
+ kind: "stale",
779
+ network: true,
780
+ candidates: [["rustup", ["check"]]],
781
+ timeout: 15000,
782
+ parse: parseRustupCheck
783
+ }
784
+ ];
785
+ var SEV_OF = {
786
+ ok: "ok",
787
+ warn: "warn",
788
+ info: "info",
789
+ skipped: "info"
790
+ };
791
+ function skippedCheck(prefix, kind, summary) {
792
+ return makeCheck(`health.${prefix}.skipped`, kind, "info", summary, "");
793
+ }
794
+ function toCheckPerEnv(prefix, envLabel, r, kind, path) {
795
+ if (r.status === "skipped") {
796
+ return makeCheck(`health.${prefix}.${envLabel}.skipped`, kind, "info", `${prefix} doctor (${envLabel}) produced no output`, "");
797
+ }
798
+ const id = `health.${prefix}.${envLabel}.check`;
799
+ const summary = r.status === "ok" ? `${prefix} check (${envLabel}): consistent` : r.summary;
800
+ return makeCheck(id, kind, SEV_OF[r.status], summary, r.detail || "", path ? [path] : []);
801
+ }
802
+ function pickCandidate(p) {
803
+ const dirs = pathDirs();
804
+ for (const [cmd, args] of p.candidates) {
805
+ if (whichInDirs(cmd, dirs).length > 0)
806
+ return [cmd, args];
807
+ }
808
+ return null;
809
+ }
810
+ function toCheckNetwork(p, r) {
811
+ const sev = SEV_OF[r.status];
812
+ const id = r.status === "skipped" ? `health.${p.id}.skipped` : `health.${p.id}.check`;
813
+ return makeCheck(id, p.kind, sev, r.summary, r.detail || "");
814
+ }
815
+ async function computeHealth(opts = {}) {
816
+ const checks = [];
817
+ const network = opts.healthNetwork === true;
818
+ const pythons = opts.pythons || [];
819
+ if (pythons.length === 0) {
820
+ checks.push(skippedCheck("pip", "conflict", "no python interpreters detected (pip check skipped)"));
821
+ } else {
822
+ await Promise.all(pythons.map(async (py) => {
823
+ const out = await runBinAsync(py.path, ["-m", "pip", "check"], 15000);
824
+ const r = parsePipCheck(out);
825
+ checks.push(toCheckPerEnv("pip", py.label, r, "conflict", py.path));
826
+ }));
827
+ }
828
+ const condaEnvs = opts.condaEnvs || [];
829
+ const condaOnPath = whichInDirs("conda", pathDirs()).length > 0;
830
+ if (!condaOnPath) {
831
+ checks.push(skippedCheck("conda", "stale", "conda not on PATH (conda doctor skipped)"));
832
+ } else if (condaEnvs.length === 0) {
833
+ checks.push(skippedCheck("conda", "stale", "no conda environments detected (conda doctor skipped)"));
834
+ } else {
835
+ await Promise.all(condaEnvs.map(async (e) => {
836
+ const out = await runAsync("conda", ["doctor", "-n", e.name], 30000);
837
+ const r = parseCondaDoctor(out);
838
+ checks.push(toCheckPerEnv("conda", e.name, r, "stale", e.pythonPath || undefined));
839
+ }));
840
+ }
841
+ for (const p of NETWORK_PROBES) {
842
+ if (p.network && !network) {
843
+ 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).`));
844
+ continue;
845
+ }
846
+ const picked = pickCandidate(p);
847
+ if (!picked)
848
+ continue;
849
+ const [cmd, args] = picked;
850
+ const out = await runAsync(cmd, args, p.timeout);
851
+ const result = p.parse(out);
852
+ checks.push(toCheckNetwork(p, result));
853
+ }
854
+ return checks;
855
+ }
856
+
857
+ // src/output/json.ts
858
+ function buildManifest(byName, conflicts, checks, environments, shells) {
859
+ const sys = byName.system || {};
860
+ const runtimes = byName.runtimes || [];
861
+ const pkg = byName.packageManagers || [];
862
+ const ides = byName.ides || [];
863
+ const issueCountBySeverity = { error: 0, warn: 0, info: 0, missing: 0, ok: 0 };
864
+ for (const c of checks)
865
+ issueCountBySeverity[c.Severity] = (issueCountBySeverity[c.Severity] || 0) + 1;
866
+ const totalInstallations = runtimes.reduce((a, g) => a + g.installs.length, 0) + pkg.reduce((a, g) => a + g.installs.length, 0) + ides.reduce((a, g) => a + g.installs.length, 0);
867
+ const env = environments || {};
868
+ const envSummary = {
869
+ vcpkgPackages: env.vcpkg?.packages?.length || 0,
870
+ pipInterpreters: env.pip?.perInterpreter?.length || 0,
871
+ pipPackages: env.pip?.perInterpreter?.reduce((a, p) => a + p.packages.length, 0) || 0,
872
+ pipOutdated: env.pip?.perInterpreter?.reduce((a, p) => a + p.outdated.length, 0) || 0,
873
+ condaEnvs: env.condaEnvs?.envs?.length || 0,
874
+ dockerInstalled: !!env.docker?.installed,
875
+ wslDistros: env.wsl?.distros?.length || 0,
876
+ vsShells: env.vsShells?.shells?.length || 0,
877
+ projectEnvs: env.projectEnvs?.envs?.length || 0
878
+ };
879
+ return {
880
+ meta: { tool: "devmap", version: VERSION, generatedAt: new Date().toISOString(), schema: "devmap.manifest/1.0", shells },
881
+ system: sys,
882
+ runtimes,
883
+ packageManagers: pkg,
884
+ ides,
885
+ environments: env,
886
+ checks,
887
+ conflicts: {
888
+ multiVersionTools: conflicts.multiVersionTools,
889
+ pathConflicts: conflicts.pathConflicts,
890
+ ghostBinaries: conflicts.ghostBinaries
891
+ },
892
+ summary: {
893
+ runtimeKinds: runtimes.length,
894
+ packageManagerKinds: pkg.length,
895
+ ideKinds: ides.length,
896
+ totalInstallations,
897
+ toolsWithMultipleVersions: conflicts.multiVersionTools.length,
898
+ issueCountBySeverity,
899
+ environments: envSummary
900
+ }
901
+ };
902
+ }
903
+
904
+ // src/glyphs.ts
905
+ var WIN = process.platform === "win32";
906
+ var G = {
907
+ spinner: WIN ? ["|", "/", "-", "\\"] : ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
908
+ dot: WIN ? "|" : "·",
909
+ arrow: WIN ? "->" : "→",
910
+ check: WIN ? "[+]" : "✓",
911
+ cross: WIN ? "[x]" : "✗",
912
+ play: WIN ? "*" : "▶",
913
+ mdash: WIN ? "-" : "—",
914
+ hrule: WIN ? "-" : "─",
915
+ scale: WIN ? "vs" : "⚖"
916
+ };
917
+ var MAP = [
918
+ [/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏]/g, ""],
919
+ [/🔍/g, ""],
920
+ [/·/g, G.dot],
921
+ [/→/g, G.arrow],
922
+ [/—/g, G.mdash],
923
+ [/─/g, G.hrule],
924
+ [/⚖/g, G.scale],
925
+ [/▶/g, G.play],
926
+ [/✓/g, G.check],
927
+ [/✗/g, G.cross]
928
+ ];
929
+ function clean(s) {
930
+ if (!WIN)
931
+ return s;
932
+ let out = s;
933
+ for (const [re, rep] of MAP)
934
+ out = out.replace(re, rep);
935
+ return out;
936
+ }
937
+
938
+ // src/progress.ts
939
+ var SPINNERS = G.spinner;
940
+
941
+ class ScanProgress {
942
+ tty;
943
+ total;
944
+ doneCount = 0;
945
+ started = Date.now();
946
+ inflight = new Set;
947
+ timings = [];
948
+ timer = null;
949
+ extra = "";
950
+ constructor(total, tty = !!process.stderr.isTTY) {
951
+ this.total = total;
952
+ this.tty = tty;
953
+ if (this.tty)
954
+ this.timer = setInterval(() => this.render(), 110);
955
+ }
956
+ setTotal(n) {
957
+ this.total = n;
958
+ }
959
+ sub(msg) {
960
+ if (!this.tty) {
961
+ process.stderr.write(" " + clean(msg) + `
962
+ `);
963
+ return;
964
+ }
965
+ this.extra = msg;
966
+ this.render();
967
+ }
968
+ start(name) {
969
+ this.inflight.add(name);
970
+ if (!this.tty)
971
+ process.stderr.write(`${G.play} ${clean(name)}
972
+ `);
973
+ else
974
+ this.render();
975
+ }
976
+ done(name, ms, err = false) {
977
+ this.inflight.delete(name);
978
+ this.doneCount++;
979
+ this.timings.push({ name, ms, err });
980
+ if (!this.tty)
981
+ process.stderr.write(`${err ? G.cross : G.check} ${clean(name)} (${(ms / 1000).toFixed(1)}s)
982
+ `);
983
+ else
984
+ this.render();
985
+ }
986
+ availWidth() {
987
+ const c = process.stderr.columns;
988
+ return typeof c === "number" && c >= 40 ? c : 90;
989
+ }
990
+ render() {
991
+ if (!this.tty)
992
+ return;
993
+ const t = ((Date.now() - this.started) / 1000).toFixed(1);
994
+ const spin = SPINNERS[Math.floor(Date.now() / 110) % SPINNERS.length];
995
+ const rawCur = [...this.inflight].join("+") + (this.extra ? G.dot + " " + this.extra : "") || G.mdash;
996
+ let cur = clean(rawCur);
997
+ const prefix = `${spin} scanning ${this.doneCount}/${this.total} ${G.dot} running: `;
998
+ const suffix = ` ${G.dot} elapsed ${t}s`;
999
+ const budget = this.availWidth() - prefix.length - suffix.length - 1;
1000
+ if (budget > 6 && cur.length > budget) {
1001
+ const ell = process.platform === "win32" ? "..." : "…";
1002
+ cur = cur.slice(0, Math.max(4, budget - ell.length)).trimEnd() + ell;
1003
+ }
1004
+ const msg = `${prefix}${cur}${suffix}`;
1005
+ process.stderr.write(`\r\x1B[K${msg}`);
1006
+ }
1007
+ finish() {
1008
+ if (this.timer) {
1009
+ clearInterval(this.timer);
1010
+ this.timer = null;
1011
+ }
1012
+ if (this.tty)
1013
+ process.stderr.write(`\r\x1B[K`);
1014
+ if (this.timings.length) {
1015
+ const w = Math.max(1, ...this.timings.map((t) => t.name.length));
1016
+ for (const t of this.timings)
1017
+ process.stderr.write(` ${t.err ? G.cross : G.check} ${clean(t.name).padEnd(w)} ${t.ms >= 1000 ? (t.ms / 1000).toFixed(1) + "s" : t.ms + "ms"}
1018
+ `);
1019
+ const total = ((Date.now() - this.started) / 1000).toFixed(1);
1020
+ process.stderr.write(` \x1B[2mscan complete, took ${total}s\x1B[0m
1021
+ `);
1022
+ }
1023
+ }
1024
+ }
1025
+
1026
+ // src/scan.ts
1027
+ import { realpathSync } from "node:fs";
1028
+ import { dirname } from "node:path";
1029
+ function healthPythonLabel(path) {
1030
+ const lower = path.toLowerCase().replace(/\\/g, "/");
1031
+ const env = lower.match(/[\\/]envs[\\/]([^\\/]+)[\\/]/);
1032
+ if (env)
1033
+ return env[1];
1034
+ if (/miniconda3[\\/]python/i.test(lower) || /anaconda3[\\/]python/i.test(lower))
1035
+ return "base";
1036
+ if (lower.includes("workbuddy/binaries/python"))
1037
+ return "managed";
1038
+ if (lower.includes("/nvm") || lower.includes("\\nvm"))
1039
+ return "nvm";
1040
+ const parts = lower.split("/").filter(Boolean);
1041
+ const parent = parts[parts.length - 2] || "";
1042
+ return parent || "python";
1043
+ }
1044
+ function dedupePythons(raw) {
1045
+ const byDir = new Map;
1046
+ for (const p of raw) {
1047
+ let rp = p.pythonPath;
1048
+ try {
1049
+ rp = realpathSync(p.pythonPath);
1050
+ } catch {}
1051
+ const dir = dirname(rp);
1052
+ if (!byDir.has(dir))
1053
+ byDir.set(dir, p.pythonPath);
1054
+ }
1055
+ const out = [];
1056
+ const used = new Set;
1057
+ for (const path of byDir.values()) {
1058
+ let label = healthPythonLabel(path);
1059
+ let n = 2;
1060
+ while (used.has(label))
1061
+ label = `${healthPythonLabel(path)}#${n++}`;
1062
+ used.add(label);
1063
+ out.push({ path, label });
1064
+ }
1065
+ return out;
1066
+ }
1067
+ async function runScan(shells, disabled, opts = {}) {
1068
+ const progress = opts.silent ? undefined : new ScanProgress(0, process.stderr.isTTY);
1069
+ const ctx = {
1070
+ shells,
1071
+ disabled,
1072
+ projectRoots: opts.projectRoots,
1073
+ progress,
1074
+ checkOutdated: opts.checkOutdated,
1075
+ wslDeep: opts.wslDeep,
1076
+ auditDeps: opts.auditDeps,
1077
+ healthNetwork: opts.healthNetwork
1078
+ };
1079
+ const { byName, allChecks } = await runDetectors(ctx, progress);
1080
+ const groups = [
1081
+ ...byName.runtimes || [],
1082
+ ...byName.packageManagers || [],
1083
+ ...byName.ides || []
1084
+ ];
1085
+ const conflicts = computeConflicts(groups, shells, disabled);
1086
+ const pythons = dedupePythons(byName.pip?.perInterpreter || []);
1087
+ const condaEnvs = (byName.condaenv?.envs || []).map((e) => ({
1088
+ name: e.name,
1089
+ pythonPath: e.pythonPath
1090
+ }));
1091
+ const healthChecks = await computeHealth({ healthNetwork: opts.healthNetwork, pythons, condaEnvs });
1092
+ const securityChecks = [
1093
+ ...computeSecurity(byName),
1094
+ ...computeScaInventory(),
1095
+ ...opts.auditDeps ? runScaAudit(process.cwd()) : []
1096
+ ];
1097
+ const collected = [...allChecks, ...conflicts.checks, ...securityChecks, ...healthChecks].filter((c) => !isCheckDisabled(c.ID, disabled));
1098
+ const environments = {
1099
+ vcpkg: byName.vcpkg,
1100
+ pip: byName.pip,
1101
+ apt: byName.apt,
1102
+ condaEnvs: byName.condaenv,
1103
+ docker: byName.docker,
1104
+ wsl: byName.wsl,
1105
+ vsShells: byName.vsshells,
1106
+ projectEnvs: byName.projects
1107
+ };
1108
+ if (progress)
1109
+ progress.finish();
1110
+ return buildManifest(byName, conflicts, collected, environments, shells);
1111
+ }
1112
+
1113
+ // src/query.ts
1114
+ var CAT_ALIASES = {
1115
+ runtimes: "runtime",
1116
+ runtime: "runtime",
1117
+ rt: "runtime",
1118
+ packagemanagers: "packageManager",
1119
+ packagemanager: "packageManager",
1120
+ pkg: "packageManager",
1121
+ pm: "packageManager",
1122
+ ides: "ide",
1123
+ ide: "ide"
1124
+ };
1125
+ var CATEGORIES = [
1126
+ ["runtimes", "runtime"],
1127
+ ["packageManagers", "packageManager"],
1128
+ ["ides", "ide"]
1129
+ ];
1130
+ function queryManifest(m, opts) {
1131
+ let groups = [];
1132
+ for (const [key, label] of CATEGORIES) {
1133
+ for (const g of m[key] || []) {
1134
+ groups.push({ id: g.id, category: label, installs: g.installs || [] });
1135
+ }
1136
+ }
1137
+ let catFilter = opts.kind || null;
1138
+ let idSub = (opts.selector || "").toLowerCase();
1139
+ if (opts.selector && opts.selector.includes(".")) {
1140
+ const dot = opts.selector.indexOf(".");
1141
+ const catPart = opts.selector.slice(0, dot).toLowerCase();
1142
+ const rest = opts.selector.slice(dot + 1).toLowerCase();
1143
+ if (catPart in CAT_ALIASES) {
1144
+ catFilter = CAT_ALIASES[catPart];
1145
+ idSub = rest;
1146
+ }
1147
+ }
1148
+ if (catFilter)
1149
+ groups = groups.filter((g) => g.category === catFilter);
1150
+ if (idSub)
1151
+ groups = groups.filter((g) => g.id.toLowerCase().includes(idSub));
1152
+ if (opts.where) {
1153
+ const w = opts.where.toLowerCase();
1154
+ groups = groups.map((g) => ({ ...g, installs: g.installs.filter((i) => (i.path || "").toLowerCase().includes(w)) })).filter((g) => g.installs.length > 0);
1155
+ }
1156
+ if (opts.offPath) {
1157
+ groups = groups.map((g) => ({ ...g, installs: g.installs.filter(isIsolated) })).filter((g) => g.installs.length > 0);
1158
+ }
1159
+ let checks = [];
1160
+ const matchesSelector = (c, s) => (c.ID || "").toLowerCase().includes(s) || (c.Label || "").toLowerCase().includes(s);
1161
+ if (opts.issues) {
1162
+ checks = m.checks || [];
1163
+ if (idSub)
1164
+ checks = checks.filter((c) => matchesSelector(c, idSub));
1165
+ } else if (idSub) {
1166
+ checks = (m.checks || []).filter((c) => matchesSelector(c, idSub));
1167
+ }
1168
+ const conflicts = opts.conflicts ? m.conflicts : undefined;
1169
+ if (opts.issues || opts.conflicts)
1170
+ groups = [];
1171
+ return { groups, checks, conflicts };
1172
+ }
1173
+
1174
+ // src/diff.ts
1175
+ var CAT_KEYS = [
1176
+ ["runtimes", "runtime"],
1177
+ ["packageManagers", "packageManager"],
1178
+ ["ides", "ide"]
1179
+ ];
1180
+ function normPath(p) {
1181
+ return (p || "").toLowerCase().replace(/\\/g, "/").replace(/\/+$/, "");
1182
+ }
1183
+ function collectInstalls(m) {
1184
+ const map = new Map;
1185
+ for (const [key, label] of CAT_KEYS) {
1186
+ for (const g of m[key] || []) {
1187
+ for (const inst of g.installs || []) {
1188
+ map.set(normPath(inst.path), {
1189
+ id: g.id,
1190
+ category: label,
1191
+ path: inst.path,
1192
+ version: inst.version || null,
1193
+ install: inst
1194
+ });
1195
+ }
1196
+ }
1197
+ }
1198
+ return map;
1199
+ }
1200
+ function ensureTool(tools, id, cat) {
1201
+ const k = id + "|" + cat;
1202
+ let t = tools.get(k);
1203
+ if (!t) {
1204
+ t = { id, category: cat, added: [], removed: [], versionChanged: [] };
1205
+ tools.set(k, t);
1206
+ }
1207
+ return t;
1208
+ }
1209
+ function diffManifests(a, b) {
1210
+ const A = collectInstalls(a);
1211
+ const B = collectInstalls(b);
1212
+ const tools = new Map;
1213
+ for (const [np, v] of B) {
1214
+ const av = A.get(np);
1215
+ const t = ensureTool(tools, v.id, v.category);
1216
+ if (!av) {
1217
+ t.added.push(v.install);
1218
+ } else if (av.version !== v.version) {
1219
+ t.versionChanged.push({ path: v.path, from: av.version, to: v.version });
1220
+ }
1221
+ }
1222
+ for (const [np, v] of A) {
1223
+ if (!B.has(np)) {
1224
+ ensureTool(tools, v.id, v.category).removed.push(v.install);
1225
+ }
1226
+ }
1227
+ const toolList = [...tools.values()].filter((t) => t.added.length || t.removed.length || t.versionChanged.length);
1228
+ const aIssues = new Map((a.checks || []).map((c) => [c.ID, c]));
1229
+ const bIssues = new Map((b.checks || []).map((c) => [c.ID, c]));
1230
+ const issues = [];
1231
+ for (const [id, c] of bIssues)
1232
+ if (!aIssues.has(id))
1233
+ issues.push({ id, added: [c], resolved: [] });
1234
+ for (const [id, c] of aIssues)
1235
+ if (!bIssues.has(id))
1236
+ issues.push({ id, added: [], resolved: [c] });
1237
+ const summary = {
1238
+ installsAdded: toolList.reduce((s, t) => s + t.added.length, 0),
1239
+ installsRemoved: toolList.reduce((s, t) => s + t.removed.length, 0),
1240
+ versionChanges: toolList.reduce((s, t) => s + t.versionChanged.length, 0),
1241
+ issuesAdded: issues.reduce((s, i) => s + i.added.length, 0),
1242
+ issuesResolved: issues.reduce((s, i) => s + i.resolved.length, 0)
1243
+ };
1244
+ return { tools: toolList, issues, summary };
1245
+ }
1246
+
1247
+ // src/detector/system.ts
1248
+ import os from "node:os";
1249
+ register({
1250
+ name: "system",
1251
+ scan() {
1252
+ const data = {
1253
+ platform: process.platform,
1254
+ arch: os.arch(),
1255
+ hostname: os.hostname(),
1256
+ release: os.release(),
1257
+ shell: process.env.SHELL || (IS_WIN ? process.env.ComSpec || "cmd.exe" : "sh"),
1258
+ totalMemGB: +(os.totalmem() / 1073741824).toFixed(1),
1259
+ cpuCount: os.cpus().length,
1260
+ pathDirCount: pathDirs().length
1261
+ };
1262
+ return { data, checks: [] };
1263
+ }
1264
+ });
1265
+
1266
+ // 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";
1270
+
1271
+ // src/detector/util.ts
1272
+ var CONCURRENCY = 12;
1273
+ var GENERIC_FLAGS = ["--version", "version", "-v", "-V"];
1274
+ function dedupe(paths, seen) {
1275
+ const out = [];
1276
+ for (const p of paths) {
1277
+ const k = p.toLowerCase();
1278
+ if (seen.has(k))
1279
+ continue;
1280
+ seen.add(k);
1281
+ out.push(p);
1282
+ }
1283
+ return out;
1284
+ }
1285
+ async function detectRuntimeAsync(id, bin, flags, multiFn) {
1286
+ const seen = new Set;
1287
+ const onPathPaths = dedupe(whichInDirs(bin, pathDirs()), seen);
1288
+ const extras = [];
1289
+ if (multiFn) {
1290
+ for (const m of await multiFn()) {
1291
+ if (!m.path)
1292
+ continue;
1293
+ const k = m.path.toLowerCase();
1294
+ if (seen.has(k))
1295
+ continue;
1296
+ seen.add(k);
1297
+ extras.push({ ...m, onPath: false });
1298
+ }
1299
+ }
1300
+ const versions = await pool(onPathPaths, CONCURRENCY, (p) => versionOfAsync(p, flags));
1301
+ const installs = onPathPaths.map((p, i) => ({ path: p, onPath: true, version: versions[i] }));
1302
+ installs.push(...extras);
1303
+ return installs.length ? { id, installs } : null;
1304
+ }
1305
+ async function detectSimpleAsync(list, onProgress) {
1306
+ const dirs = pathDirs();
1307
+ const byName = new Map;
1308
+ const jobs = [];
1309
+ for (const name of list) {
1310
+ const paths = dedupe(whichInDirs(name, dirs), new Set);
1311
+ if (!paths.length)
1312
+ continue;
1313
+ byName.set(name, paths);
1314
+ for (const p of paths)
1315
+ jobs.push({ name, path: p });
1316
+ }
1317
+ let done = 0;
1318
+ const versions = await pool(jobs, CONCURRENCY, async (j) => {
1319
+ const v = await versionOfAsync(j.path, GENERIC_FLAGS);
1320
+ onProgress?.(++done, jobs.length);
1321
+ return v;
1322
+ });
1323
+ const vmap = new Map;
1324
+ jobs.forEach((j, i) => vmap.set(`${j.name}\x00${j.path}`, versions[i]));
1325
+ const out = [];
1326
+ for (const name of list) {
1327
+ const paths = byName.get(name);
1328
+ if (!paths)
1329
+ continue;
1330
+ out.push({
1331
+ id: name,
1332
+ installs: paths.map((p) => ({ path: p, onPath: true, version: vmap.get(`${name}\x00${p}`) ?? null }))
1333
+ });
1334
+ }
1335
+ return out;
1336
+ }
1337
+
1338
+ // src/detector/runtimes.ts
1339
+ var HOME = homedir();
1340
+ async function condaPythonPaths() {
1341
+ const found = [];
1342
+ const bases = new Set;
1343
+ const ci = await runAsync("conda", ["info", "--base"], 15000);
1344
+ if (ci)
1345
+ bases.add(ci.trim());
1346
+ bases.add(join2(HOME, "miniconda3"));
1347
+ bases.add(join2(HOME, "anaconda3"));
1348
+ bases.add("E:\\Users\\Administrator\\miniconda3");
1349
+ for (const base of bases) {
1350
+ if (!base || !existsSync2(base))
1351
+ continue;
1352
+ const basePy = IS_WIN ? join2(base, "python.exe") : join2(base, "bin", "python");
1353
+ if (existsSync2(basePy))
1354
+ 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))
1360
+ found.push({ path: py, env: e });
1361
+ }
1362
+ }
1363
+ }
1364
+ return found;
1365
+ }
1366
+ async function scanCondaPythons() {
1367
+ const paths = await condaPythonPaths();
1368
+ const versions = await pool(paths, 12, (p) => versionOfAsync(p.path, ["--version", "-V"]));
1369
+ return paths.map((p, i) => ({ path: p.path, onPath: false, version: versions[i], env: p.env }));
1370
+ }
1371
+ async function scanNvmNodes() {
1372
+ const cands = [join2(HOME, "AppData", "Roaming", "nvm"), join2(HOME, ".nvm", "versions", "node")];
1373
+ const found = [];
1374
+ for (const dir of cands) {
1375
+ if (!existsSync2(dir))
1376
+ 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))
1380
+ found.push({ path: bin, env: v });
1381
+ }
1382
+ }
1383
+ const versions = await pool(found, 12, (f) => versionOfAsync(f.path, ["--version"]));
1384
+ return found.map((f, i) => ({ path: f.path, onPath: false, version: versions[i], env: f.env }));
1385
+ }
1386
+ async function scanKnownBinary(cands, flags) {
1387
+ const hits = cands.filter((c) => existsSync2(c));
1388
+ const versions = await pool(hits, 12, (c) => versionOfAsync(c, flags));
1389
+ return hits.map((c, i) => ({ path: c, onPath: false, version: versions[i] }));
1390
+ }
1391
+ register({
1392
+ name: "runtimes",
1393
+ async scan(ctx) {
1394
+ ctx.progress?.sub("runtimes · probing 7 runtimes concurrently");
1395
+ const results = await Promise.all([
1396
+ detectRuntimeAsync("node", "node", ["--version"], scanNvmNodes),
1397
+ detectRuntimeAsync("python", "python", ["--version", "-V"], scanCondaPythons),
1398
+ detectRuntimeAsync("go", "go", ["version"]),
1399
+ detectRuntimeAsync("rustc", "rustc", ["--version"]),
1400
+ detectRuntimeAsync("java", "java", ["-version"]),
1401
+ detectRuntimeAsync("ruby", "ruby", ["--version"]),
1402
+ detectRuntimeAsync("bun", "bun", ["--version"], () => scanKnownBinary([join2(HOME, ".bun", "bin", "bun"), join2(HOME, ".bun", "bin", "bun.exe")], ["--version"]))
1403
+ ]);
1404
+ const groups = results.filter((g) => g !== null);
1405
+ return { data: groups, checks: [] };
1406
+ }
1407
+ });
1408
+
1409
+ // src/detector/pkgmgrs.ts
1410
+ var TOOLS = [
1411
+ "npm",
1412
+ "pip",
1413
+ "pip3",
1414
+ "conda",
1415
+ "cargo",
1416
+ "brew",
1417
+ "winget",
1418
+ "scoop",
1419
+ "docker",
1420
+ "kubectl",
1421
+ "git",
1422
+ "poetry",
1423
+ "yarn",
1424
+ "pnpm",
1425
+ "pyenv",
1426
+ "nvm"
1427
+ ];
1428
+ register({
1429
+ name: "packageManagers",
1430
+ async scan(ctx) {
1431
+ const data = await detectSimpleAsync(TOOLS, (done, total) => {
1432
+ ctx.progress?.sub(`packageManagers · ${done}/${total}`);
1433
+ });
1434
+ return { data, checks: [] };
1435
+ }
1436
+ });
1437
+
1438
+ // src/detector/ides.ts
1439
+ register({
1440
+ name: "ides",
1441
+ async scan(ctx) {
1442
+ const data = await detectSimpleAsync(["code", "cursor", "pycharm", "idea"], (done, total) => {
1443
+ ctx.progress?.sub(`ides · ${done}/${total}`);
1444
+ });
1445
+ return { data, checks: [] };
1446
+ }
1447
+ });
1448
+
1449
+ // src/detector/vcpkg.ts
1450
+ import { join as join3 } from "node:path";
1451
+ var CANDIDATES = [
1452
+ "C:\\vcpkg\\vcpkg.exe",
1453
+ "C:\\src\\vcpkg\\vcpkg.exe",
1454
+ "C:\\tools\\vcpkg\\vcpkg.exe",
1455
+ "C:\\vcpkg\\vcpkg"
1456
+ ];
1457
+ function parseVcpkgVersion(out) {
1458
+ if (!out)
1459
+ return null;
1460
+ const m = out.match(/version\s+(\d{4}-\d{2}-\d{2}(?:-[0-9a-f]+)?)/i);
1461
+ return m ? m[1] : null;
1462
+ }
1463
+ async function findVcpkg() {
1464
+ const cands = [];
1465
+ const seen = new Set;
1466
+ const add = (p, onPath) => {
1467
+ const k = p.toLowerCase();
1468
+ if (seen.has(k))
1469
+ return;
1470
+ seen.add(k);
1471
+ cands.push({ path: p, onPath });
1472
+ };
1473
+ for (const c of CANDIDATES)
1474
+ if (fileExists(c))
1475
+ add(c, false);
1476
+ if (process.env.VCPKG_ROOT) {
1477
+ const p = join3(process.env.VCPKG_ROOT, IS_WIN ? "vcpkg.exe" : "vcpkg");
1478
+ if (fileExists(p))
1479
+ add(p, false);
1480
+ }
1481
+ for (const p of whichInDirs("vcpkg", pathDirs()))
1482
+ add(p, true);
1483
+ const versions = await pool(cands, 8, (c) => runBinAsync(c.path, ["version"], 9000).then(parseVcpkgVersion));
1484
+ return cands.map((c, i) => ({ path: c.path, onPath: c.onPath, version: versions[i] }));
1485
+ }
1486
+ function parseList(out) {
1487
+ const pkgs = [];
1488
+ for (const line of out.split(`
1489
+ `)) {
1490
+ const m = line.match(/^(\S+?):(\S+)\s+(\S+)/);
1491
+ if (m)
1492
+ pkgs.push({ name: m[1], triplet: m[2], version: m[3] });
1493
+ }
1494
+ return pkgs;
1495
+ }
1496
+ register({
1497
+ name: "vcpkg",
1498
+ async scan() {
1499
+ const installs = await findVcpkg();
1500
+ const checks = [];
1501
+ if (!installs.length)
1502
+ return { data: { installed: false, installs: [], packages: [] }, checks };
1503
+ const primary = installs[0].path;
1504
+ const raw = await runBinAsync(primary, ["list"], 15000);
1505
+ const fallback = raw ? "" : IS_WIN ? await runAsync("cmd", ["/c", primary, "list"], 15000) : await runAsync(primary, ["list"], 15000);
1506
+ const packages = parseList(raw || fallback);
1507
+ if (installs.length > 1) {
1508
+ checks.push(makeCheck("version.multi.vcpkg", "conflict", "warn", `vcpkg has ${installs.length} installed copies`, `Multiple vcpkg copies can mix different triplet libraries; active → ${installs[0].path}; others: ${installs.slice(1).map((i) => i.path).join(", ")}`, installs.map((i) => i.path)));
1509
+ }
1510
+ return { data: { installed: true, installs, packages }, checks };
1511
+ }
1512
+ });
1513
+
1514
+ // src/detector/piputil.ts
1515
+ function parseList2(raw) {
1516
+ if (!raw)
1517
+ return [];
1518
+ try {
1519
+ const arr = JSON.parse(raw);
1520
+ return Array.isArray(arr) ? arr.map((p) => ({ name: p.name, version: p.version || "?" })) : [];
1521
+ } catch {
1522
+ return [];
1523
+ }
1524
+ }
1525
+ function parseOutdated(raw) {
1526
+ if (!raw)
1527
+ return [];
1528
+ try {
1529
+ const arr = JSON.parse(raw);
1530
+ return Array.isArray(arr) ? arr.map((p) => ({ name: p.name, installed: p.version || "?", latest: p.latest_version || "?" })) : [];
1531
+ } catch {
1532
+ return [];
1533
+ }
1534
+ }
1535
+ async function pipPackagesAsync(pythonPath, timeout = 6000, withOutdated = false) {
1536
+ try {
1537
+ const packages = parseList2(await runBinAsync(pythonPath, ["-m", "pip", "list", "--format", "json"], timeout));
1538
+ let outdated = [];
1539
+ if (withOutdated) {
1540
+ outdated = parseOutdated(await runBinAsync(pythonPath, ["-m", "pip", "list", "--outdated", "--format", "json"], timeout));
1541
+ }
1542
+ return { packages, outdated };
1543
+ } catch {
1544
+ return { packages: [], outdated: [], error: "pip list failed" };
1545
+ }
1546
+ }
1547
+
1548
+ // src/detector/pip.ts
1549
+ import { basename as basename2 } from "node:path";
1550
+ function shortName(pythonPath) {
1551
+ const lower = pythonPath.toLowerCase();
1552
+ const m = lower.match(/[\\/]envs[\\/]([^\\/]+)[\\/]/);
1553
+ if (m)
1554
+ return m[1];
1555
+ if (/miniconda3[\\/]python/i.test(lower) || /anaconda3[\\/]python/i.test(lower))
1556
+ return "base";
1557
+ return basename2(pythonPath).replace(/\.exe$/i, "").replace(/\.cmd$/i, "");
1558
+ }
1559
+ async function findPythons() {
1560
+ const found = [];
1561
+ const seen = new Set;
1562
+ const add = (p, onPath) => {
1563
+ const k = p.toLowerCase();
1564
+ if (seen.has(k))
1565
+ return;
1566
+ seen.add(k);
1567
+ found.push({ path: p, onPath });
1568
+ };
1569
+ for (const p of whichInDirs("python", pathDirs()))
1570
+ add(p, true);
1571
+ for (const p of whichInDirs("python3", pathDirs()))
1572
+ add(p, true);
1573
+ for (const inst of await condaPythonPaths())
1574
+ add(inst.path, false);
1575
+ return found;
1576
+ }
1577
+ register({
1578
+ name: "pip",
1579
+ async scan(ctx) {
1580
+ const pythons = await findPythons();
1581
+ const perInterpreter = [];
1582
+ const checks = [];
1583
+ const withOutdated = ctx.checkOutdated === true;
1584
+ let done = 0;
1585
+ const results = await pool(pythons, 6, async (py) => {
1586
+ const r = await pipPackagesAsync(py.path, withOutdated ? 20000 : 8000, withOutdated);
1587
+ ctx.progress?.sub(`pip · ${++done}/${pythons.length} interpreters`);
1588
+ return r;
1589
+ });
1590
+ pythons.forEach((py, i) => {
1591
+ const r = results[i];
1592
+ const short = shortName(py.path);
1593
+ perInterpreter.push({ pythonPath: py.path, version: null, packages: r.packages, outdated: r.outdated });
1594
+ if (r.outdated.length) {
1595
+ checks.push(makeCheck(`pip.outdated.${short}`, "stale", "info", `python(${short}) has ${r.outdated.length} upgradable packages`, `e.g.: ${r.outdated.slice(0, 5).map((o) => `${o.name} ${o.installed}→${o.latest}`).join(", ")}`, [py.path]));
1596
+ }
1597
+ });
1598
+ return { data: { perInterpreter }, checks };
1599
+ }
1600
+ });
1601
+
1602
+ // src/detector/apt.ts
1603
+ function parseApt(out) {
1604
+ const pkgs = [];
1605
+ for (const line of out.split(`
1606
+ `)) {
1607
+ const m = line.match(/^([^:\s]+)(?::[^\s]+)?\S*\s+(\S+)\s/);
1608
+ if (m)
1609
+ pkgs.push({ name: m[1], version: m[2] });
1610
+ }
1611
+ return pkgs;
1612
+ }
1613
+ register({
1614
+ name: "apt",
1615
+ async scan() {
1616
+ const checks = [];
1617
+ if (IS_WIN)
1618
+ return { data: { available: false, count: 0, packages: [], upgradable: 0 }, checks };
1619
+ const [installedRaw, upRaw] = await Promise.all([
1620
+ runAsync("apt", ["list", "--installed"], 20000),
1621
+ runAsync("apt", ["list", "--upgradable"], 15000)
1622
+ ]);
1623
+ const packages = installedRaw ? parseApt(installedRaw) : [];
1624
+ const upgradable = upRaw ? parseApt(upRaw).length : 0;
1625
+ if (upgradable > 0) {
1626
+ checks.push(makeCheck("apt.upgradable", "stale", "info", `apt has ${upgradable} upgradable packages`, `Run sudo apt upgrade to update; or apt list --upgradable to view`, []));
1627
+ }
1628
+ return { data: { available: true, count: packages.length, packages, upgradable }, checks };
1629
+ }
1630
+ });
1631
+
1632
+ // src/detector/condaenv.ts
1633
+ import { join as join4 } from "node:path";
1634
+ import { existsSync as existsSync3 } from "node:fs";
1635
+ function pythonOf(envPath) {
1636
+ const py = IS_WIN ? join4(envPath, "python.exe") : join4(envPath, "bin", "python");
1637
+ return existsSync3(py) ? py : null;
1638
+ }
1639
+ register({
1640
+ name: "condaenv",
1641
+ async scan(ctx) {
1642
+ const out = await runAsync("conda", ["env", "list"], 15000);
1643
+ const checks = [];
1644
+ if (!out)
1645
+ return { data: { envs: [] }, checks };
1646
+ const parsed = [];
1647
+ for (const line of out.split(/\r?\n/)) {
1648
+ const t = line.trim();
1649
+ if (!t || t.startsWith("#"))
1650
+ continue;
1651
+ const m = t.match(/^(\S+)\s+(\*?)\s*(.+)$/);
1652
+ if (!m)
1653
+ continue;
1654
+ parsed.push({ name: m[1], active: m[2] === "*", py: pythonOf(m[3].trim()) });
1655
+ }
1656
+ const withOutdated = ctx.checkOutdated === true;
1657
+ let done = 0;
1658
+ const results = await pool(parsed, 6, async (e) => {
1659
+ const r = e.py ? await pipPackagesAsync(e.py, withOutdated ? 20000 : 8000, withOutdated) : { packages: [], outdated: [] };
1660
+ ctx.progress?.sub(`conda env · ${++done}/${parsed.length}`);
1661
+ return r;
1662
+ });
1663
+ const envs = [];
1664
+ let anyOutdated = 0;
1665
+ parsed.forEach((e, i) => {
1666
+ const r = results[i];
1667
+ anyOutdated += r.outdated.length;
1668
+ envs.push({ name: e.name, pythonPath: e.py, active: e.active, packages: r.packages, outdated: r.outdated });
1669
+ });
1670
+ if (anyOutdated > 0) {
1671
+ checks.push(makeCheck("conda.env.outdated", "stale", "info", `conda envs have ${anyOutdated} upgradable packages`, `Use conda env export / pip list --outdated to view per-env details`, envs.map((e) => e.pythonPath || e.name)));
1672
+ }
1673
+ return { data: { envs }, checks };
1674
+ }
1675
+ });
1676
+
1677
+ // src/detector/docker.ts
1678
+ var DESKTOP = "C:\\Program Files\\Docker\\Docker\\resources\\bin\\docker.exe";
1679
+ function findDocker() {
1680
+ for (const p of whichInDirs("docker", pathDirs()))
1681
+ return p;
1682
+ if (IS_WIN && fileExists(DESKTOP))
1683
+ return DESKTOP;
1684
+ return null;
1685
+ }
1686
+ function countLines(out) {
1687
+ if (!out)
1688
+ return 0;
1689
+ return out.split(`
1690
+ `).filter((l) => l.trim().length).length;
1691
+ }
1692
+ register({
1693
+ name: "docker",
1694
+ async scan() {
1695
+ const checks = [];
1696
+ const bin = findDocker();
1697
+ if (!bin)
1698
+ return { data: { installed: false, daemonRunning: null, engineVersion: null, images: 0, containers: 0 }, checks };
1699
+ const [version, info] = await Promise.all([
1700
+ versionOfAsync(bin, ["--version"]),
1701
+ runBinAsync(bin, ["info", "--format", "{{.ServerVersion}}"], 8000)
1702
+ ]);
1703
+ const daemonRunning = !!info;
1704
+ const engineVersion = info || null;
1705
+ let images = 0;
1706
+ let containers = 0;
1707
+ if (daemonRunning) {
1708
+ const [imgRaw, psRaw] = await Promise.all([
1709
+ runBinAsync(bin, ["images", "-q"], 8000),
1710
+ runBinAsync(bin, ["ps", "-a", "-q"], 8000)
1711
+ ]);
1712
+ images = countLines(imgRaw);
1713
+ containers = countLines(psRaw);
1714
+ }
1715
+ if (!daemonRunning) {
1716
+ checks.push(makeCheck("docker.daemon.down", "missing", "warn", "docker installed but daemon not running", "Docker Desktop not started or dockerd not running; images/containers cannot be enumerated", [bin]));
1717
+ }
1718
+ return { data: { installed: true, daemonRunning, engineVersion, images, containers }, checks };
1719
+ }
1720
+ });
1721
+
1722
+ // src/detector/wsl.ts
1723
+ var PROBE_SCRIPT = "echo py3=$(command -v python3); echo py=$(command -v python); echo node=$(command -v node); " + "echo go=$(command -v go); echo rustc=$(command -v rustc); echo gcc=$(command -v gcc); " + "echo git=$(command -v git); echo docker=$(command -v docker)";
1724
+ var TOOL_KEYS = ["py3", "py", "node", "go", "rustc", "gcc", "git", "docker"];
1725
+ async function probeDistro(name) {
1726
+ const tools = {};
1727
+ for (const k of TOOL_KEYS)
1728
+ tools[k] = null;
1729
+ const out = await runAsync("wsl", ["-d", name, "--", "bash", "-lc", PROBE_SCRIPT], 20000);
1730
+ if (!out)
1731
+ return tools;
1732
+ for (const line of out.split(/\r?\n/)) {
1733
+ const m = line.trim().match(/^(\w+)=(.*)$/);
1734
+ if (m && m[1] in tools)
1735
+ tools[m[1]] = m[2].trim() || null;
1736
+ }
1737
+ return tools;
1738
+ }
1739
+ async function distroBasePaths() {
1740
+ const map = new Map;
1741
+ if (!IS_WIN)
1742
+ return map;
1743
+ const root = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Lxss";
1744
+ const keysRaw = await runAsync("reg", ["query", root], 5000);
1745
+ if (!keysRaw)
1746
+ return map;
1747
+ const keys = keysRaw.split(/\r?\n/).map((l) => l.trim()).filter((k) => /\\\{[0-9a-fA-F-]+\}$/.test(k));
1748
+ const details = await pool(keys, 8, (k) => runAsync("reg", ["query", k], 5000));
1749
+ for (const detail of details) {
1750
+ if (!detail)
1751
+ continue;
1752
+ const nameM = detail.match(/DistributionName\s+REG_SZ\s+(.+)/);
1753
+ const baseM = detail.match(/BasePath\s+REG_SZ\s+(.+)/);
1754
+ if (nameM)
1755
+ map.set(nameM[1].trim(), baseM ? baseM[1].trim() : "");
1756
+ }
1757
+ return map;
1758
+ }
1759
+ function parseList3(raw) {
1760
+ const distros = [];
1761
+ for (const line of raw.split(/\r?\n/)) {
1762
+ const t = line.replace(/\u0000/g, "").trimEnd();
1763
+ if (!t.trim())
1764
+ continue;
1765
+ if (/^\s*\*?\s*NAME\s+STATE\s+VERSION/i.test(t))
1766
+ continue;
1767
+ if (/^[\s-]+$/.test(t))
1768
+ continue;
1769
+ const m = t.match(/^(\*?)\s*(\S.*?)\s{2,}(\S+)\s+(\d+)\s*$/);
1770
+ if (!m)
1771
+ continue;
1772
+ distros.push({ name: m[2].trim(), state: m[3].trim(), version: m[4], default: m[1] === "*" });
1773
+ }
1774
+ return distros;
1775
+ }
1776
+ register({
1777
+ name: "wsl",
1778
+ async scan(ctx) {
1779
+ const checks = [];
1780
+ if (!IS_WIN)
1781
+ return { data: { available: false, distros: [] }, checks };
1782
+ const raw = await runAsync("wsl", ["-l", "-v"], 8000);
1783
+ if (!raw)
1784
+ return { data: { available: false, distros: [] }, checks };
1785
+ const distros = parseList3(raw);
1786
+ const bases = await distroBasePaths();
1787
+ for (const d of distros) {
1788
+ const bp = bases.get(d.name);
1789
+ if (bp)
1790
+ d.basePath = bp;
1791
+ }
1792
+ const deep = ctx.wslDeep === true;
1793
+ const targets = distros.filter((d) => deep || /running/i.test(d.state));
1794
+ if (targets.length) {
1795
+ ctx.progress?.sub(`wsl · probing ${targets.length} distros`);
1796
+ const toolsList = await pool(targets, 4, (d) => probeDistro(d.name));
1797
+ targets.forEach((d, i) => {
1798
+ d.tools = toolsList[i];
1799
+ d.probed = true;
1800
+ });
1801
+ }
1802
+ for (const d of distros)
1803
+ if (!d.probed)
1804
+ d.probed = false;
1805
+ if (distros.length && !distros.some((d) => d.default)) {
1806
+ checks.push(makeCheck("wsl.no-default", "mismatch", "info", "WSL has no default distro", "Set one with wsl --set-default <name>", distros.map((d) => d.name)));
1807
+ }
1808
+ const skipped = distros.filter((d) => !d.probed);
1809
+ if (skipped.length) {
1810
+ checks.push(makeCheck("wsl.not-probed", "mismatch", "info", `${skipped.length} WSL distros were not probed for internal tools (not running)`, "Stopped distros are not woken up by default, to keep the scan fast; pass --wsl-deep to probe them all", skipped.map((d) => `${d.name}(${d.state})`)));
1811
+ }
1812
+ return { data: { available: true, distros }, checks };
1813
+ }
1814
+ });
1815
+
1816
+ // 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";
1819
+ var VS_BASES = [
1820
+ "C:\\Program Files\\Microsoft Visual Studio",
1821
+ "C:\\Program Files (x86)\\Microsoft Visual Studio"
1822
+ ];
1823
+ var START_MENU = [
1824
+ "C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs",
1825
+ "C:\\Users\\Administrator\\AppData\\Roaming\\Microsoft\\Windows\\Start Menu\\Programs"
1826
+ ];
1827
+ var EDITIONS = ["Community", "Professional", "Enterprise", "BuildTools"];
1828
+ function vsRoots() {
1829
+ const roots = [];
1830
+ for (const b of VS_BASES) {
1831
+ if (!existsSync4(b))
1832
+ continue;
1833
+ for (const v of readdirSync3(b)) {
1834
+ const verDir = join5(b, v);
1835
+ if (!existsSync4(verDir) || !statSync2(verDir).isDirectory())
1836
+ continue;
1837
+ const hasEdition = EDITIONS.some((e) => {
1838
+ const ed = join5(verDir, e);
1839
+ return existsSync4(ed) && statSync2(ed).isDirectory();
1840
+ });
1841
+ if (hasEdition) {
1842
+ for (const e of EDITIONS) {
1843
+ const ed = join5(verDir, e);
1844
+ if (existsSync4(ed) && statSync2(ed).isDirectory())
1845
+ roots.push(ed);
1846
+ }
1847
+ } else {
1848
+ roots.push(verDir);
1849
+ }
1850
+ }
1851
+ }
1852
+ return roots;
1853
+ }
1854
+ function msvcVersionsDesc(msvc) {
1855
+ let vers = [];
1856
+ try {
1857
+ vers = readdirSync3(msvc);
1858
+ } catch {
1859
+ return [];
1860
+ }
1861
+ return vers.sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
1862
+ }
1863
+ function vsCl(root, arch) {
1864
+ const msvc = join5(root, "VC", "Tools", "MSVC");
1865
+ if (!existsSync4(msvc))
1866
+ return null;
1867
+ for (const v of msvcVersionsDesc(msvc)) {
1868
+ const p = join5(msvc, v, "bin", `Host${arch}`, arch, "cl.exe");
1869
+ if (existsSync4(p))
1870
+ return p;
1871
+ }
1872
+ return null;
1873
+ }
1874
+ function vsMsbuild(root) {
1875
+ const base = join5(root, "MSBuild");
1876
+ if (!existsSync4(base))
1877
+ return null;
1878
+ const cands = [join5(base, "Current", "Bin", "MSBuild.exe")];
1879
+ try {
1880
+ for (const v of readdirSync3(base))
1881
+ cands.push(join5(base, v, "Bin", "MSBuild.exe"));
1882
+ } catch {}
1883
+ return cands.find((c) => existsSync4(c)) || null;
1884
+ }
1885
+ function vsCMake(root) {
1886
+ const p = join5(root, "Common7", "IDE", "CommonExtensions", "Microsoft", "CMake", "CMake", "bin", "cmake.exe");
1887
+ return existsSync4(p) ? p : null;
1888
+ }
1889
+ function vsDotnet() {
1890
+ return whichInDirs("dotnet", pathDirs())[0] || null;
1891
+ }
1892
+ function scanToolchain() {
1893
+ const out = { cl_x64: null, cl_x86: null, cl_arm64: null, msbuild: null, cmake: null, dotnet: vsDotnet() };
1894
+ for (const r of vsRoots()) {
1895
+ if (!out.cl_x64)
1896
+ out.cl_x64 = vsCl(r, "x64");
1897
+ if (!out.cl_x86)
1898
+ out.cl_x86 = vsCl(r, "x86");
1899
+ if (!out.cl_arm64)
1900
+ out.cl_arm64 = vsCl(r, "arm64");
1901
+ if (!out.msbuild)
1902
+ out.msbuild = vsMsbuild(r);
1903
+ if (!out.cmake)
1904
+ out.cmake = vsCMake(r);
1905
+ }
1906
+ return out;
1907
+ }
1908
+ function shortcutNames() {
1909
+ const names = [];
1910
+ const stack = [...START_MENU.filter((d) => existsSync4(d))];
1911
+ while (stack.length) {
1912
+ const dir = stack.pop();
1913
+ let entries = [];
1914
+ try {
1915
+ entries = readdirSync3(dir);
1916
+ } catch {
1917
+ continue;
1918
+ }
1919
+ for (const e of entries) {
1920
+ const full = join5(dir, e);
1921
+ try {
1922
+ if (statSync2(full).isDirectory())
1923
+ stack.push(full);
1924
+ else if (e.toLowerCase().endsWith(".lnk") && /developer|native tools|vs 20/i.test(e))
1925
+ names.push(e.replace(/\.lnk$/i, ""));
1926
+ } catch {}
1927
+ }
1928
+ }
1929
+ return names;
1930
+ }
1931
+ function inferKindArch(name) {
1932
+ const kind = /powershell/i.test(name) ? "powershell" : "command";
1933
+ const am = name.match(/x64|x86|arm64/i);
1934
+ const arch = am ? am[0].toLowerCase() : "auto";
1935
+ return { kind, arch };
1936
+ }
1937
+ function entryRef(kind) {
1938
+ const fn = kind === "powershell" ? "Launch-VsDevShell.ps1" : "VsDevCmd.bat";
1939
+ for (const r of vsRoots()) {
1940
+ const p = join5(r, "Common7", "Tools", fn);
1941
+ if (existsSync4(p))
1942
+ return p;
1943
+ }
1944
+ return null;
1945
+ }
1946
+ register({
1947
+ name: "vsshells",
1948
+ scan() {
1949
+ const checks = [];
1950
+ if (!IS_WIN)
1951
+ return { data: { shells: [] }, checks };
1952
+ const tc = scanToolchain();
1953
+ const names = shortcutNames();
1954
+ const shells = [];
1955
+ const seen = new Set;
1956
+ const anyCl = !!(tc.cl_x64 || tc.cl_x86 || tc.cl_arm64);
1957
+ for (const name of names) {
1958
+ const { kind, arch } = inferKindArch(name);
1959
+ const key = `${kind}:${arch}`;
1960
+ if (seen.has(key))
1961
+ continue;
1962
+ seen.add(key);
1963
+ const clPath = arch === "auto" ? tc.cl_x64 : tc[`cl_${arch}`];
1964
+ const tools = {
1965
+ cl: clPath,
1966
+ msbuild: tc.msbuild,
1967
+ cmake: tc.cmake,
1968
+ dotnet: tc.dotnet
1969
+ };
1970
+ shells.push({ name, kind, arch, entry: entryRef(kind), pathDirs: Object.values(tools).filter(Boolean).length, tools });
1971
+ }
1972
+ if (shells.length) {
1973
+ checks.push(makeCheck("vs.shells", "mismatch", "info", `Found ${shells.length} Visual Studio dev shells`, shells.map((s) => s.name).join("; "), shells.map((s) => s.entry || s.name)));
1974
+ }
1975
+ if (shells.length && !anyCl) {
1976
+ checks.push(makeCheck("vs.no-vc-tools", "missing", "warn", 'Visual Studio is installed but no cl.exe was found (the "Desktop development with C++" workload may be missing)', "Dev shells will have no MSVC toolchain; select the C++ workload in the Visual Studio Installer", vsRoots()));
1977
+ }
1978
+ return { data: { shells }, checks };
1979
+ }
1980
+ });
1981
+
1982
+ // 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";
1985
+ function isVenv(dir) {
1986
+ if (!existsSync5(join6(dir, "pyvenv.cfg")))
1987
+ return false;
1988
+ const child = IS_WIN_PYVENV(dir);
1989
+ return child;
1990
+ }
1991
+ function IS_WIN_PYVENV(dir) {
1992
+ return existsSync5(join6(dir, "Scripts", "activate")) || existsSync5(join6(dir, "Scripts", "activate.bat")) || existsSync5(join6(dir, "bin", "activate"));
1993
+ }
1994
+ function walk(root, maxDepth, out, seen) {
1995
+ const stack = [[root, 0]];
1996
+ while (stack.length) {
1997
+ const [dir, depth] = stack.pop();
1998
+ let entries = [];
1999
+ try {
2000
+ entries = readdirSync4(dir);
2001
+ } catch {
2002
+ continue;
2003
+ }
2004
+ for (const e of entries) {
2005
+ if (e === "node_modules" || e === ".git" || e === "target" || e === "dist" || e === ".next" || e === "out") {
2006
+ if (e === "node_modules") {
2007
+ const p = join6(dir, e);
2008
+ if (!seen.has(p.toLowerCase())) {
2009
+ seen.add(p.toLowerCase());
2010
+ out.push({ project: basename3(dir), type: "node_modules", path: p });
2011
+ }
2012
+ }
2013
+ continue;
2014
+ }
2015
+ const full = join6(dir, e);
2016
+ let st;
2017
+ try {
2018
+ st = statSync3(full);
2019
+ } catch {
2020
+ continue;
2021
+ }
2022
+ if (!st.isDirectory())
2023
+ continue;
2024
+ if (e === ".venv" || e === "venv") {
2025
+ if (isVenv(full) && !seen.has(full.toLowerCase())) {
2026
+ seen.add(full.toLowerCase());
2027
+ out.push({ project: basename3(dirname2(full)), type: "venv", path: full });
2028
+ }
2029
+ continue;
2030
+ }
2031
+ if (depth < maxDepth)
2032
+ stack.push([full, depth + 1]);
2033
+ }
2034
+ }
2035
+ }
2036
+ register({
2037
+ name: "projects",
2038
+ scan(ctx) {
2039
+ const roots = ctx.projectRoots && ctx.projectRoots.length ? ctx.projectRoots : [process.cwd()];
2040
+ const envs = [];
2041
+ const seen = new Set;
2042
+ for (const r of roots) {
2043
+ if (existsSync5(r))
2044
+ walk(r, 4, envs, seen);
2045
+ }
2046
+ const checks = [];
2047
+ if (envs.length) {
2048
+ const venv = envs.filter((e) => e.type === "venv").length;
2049
+ const nm = envs.filter((e) => e.type === "node_modules").length;
2050
+ checks.push(makeCheck("project.envs", "mismatch", "info", `Found ${envs.length} project-local environment${envs.length === 1 ? "" : "s"} across ${roots.length} root director${roots.length === 1 ? "y" : "ies"} (venv×${venv} / node_modules×${nm})`, envs.slice(0, 10).map((e) => `${e.project}:${e.type}`).join(", "), envs.map((e) => e.path)));
2051
+ }
2052
+ return { data: { roots, envs }, checks };
2053
+ }
2054
+ });
2055
+
2056
+ // 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";
2060
+ import { execSync } from "node:child_process";
2061
+ if (process.platform === "win32") {
2062
+ try {
2063
+ execSync("chcp 65001 > nul 2>&1", { stdio: "ignore" });
2064
+ } catch {}
2065
+ }
2066
+ if (process.platform === "win32") {
2067
+ const olog = console.log.bind(console);
2068
+ console.log = (...a) => olog(...a.map((x) => typeof x === "string" ? clean(x) : x));
2069
+ const oerr = console.error.bind(console);
2070
+ console.error = (...a) => oerr(...a.map((x) => typeof x === "string" ? clean(x) : x));
2071
+ }
2072
+ var ALL_SHELLS = ["current", "powershell", "gitbash", "wsl"];
2073
+ var DEFAULT_MANIFEST = join7(homedir2(), ".devmap", "devmap.json");
2074
+ function emitText(text, withBom = false) {
2075
+ const prefix = withBom && !process.stdout.isTTY ? "\uFEFF" : "";
2076
+ process.stdout.write(prefix + text + `
2077
+ `);
2078
+ }
2079
+ function tokenize(argv) {
2080
+ const pos = [];
2081
+ const opts = {};
2082
+ for (let i = 0;i < argv.length; i++) {
2083
+ const a = argv[i];
2084
+ if (a.startsWith("--")) {
2085
+ const eq = a.indexOf("=");
2086
+ if (eq >= 0) {
2087
+ opts[a.slice(2, eq)] = a.slice(eq + 1);
2088
+ continue;
2089
+ }
2090
+ const key = a.slice(2);
2091
+ const nxt = argv[i + 1];
2092
+ if (nxt !== undefined && !nxt.startsWith("--")) {
2093
+ opts[key] = opts[key] !== undefined ? `${opts[key]},${nxt}` : nxt;
2094
+ i++;
2095
+ } else
2096
+ opts[key] = true;
2097
+ } else if (a.startsWith("-") && a.length > 1) {
2098
+ opts[a.slice(1)] = true;
2099
+ } else
2100
+ pos.push(a);
2101
+ }
2102
+ return { pos, opts };
2103
+ }
2104
+ function toShells(v) {
2105
+ if (typeof v !== "string")
2106
+ return ["current"];
2107
+ const list = v.split(",").map((s) => s.trim());
2108
+ return list.includes("all") ? ALL_SHELLS : list.length ? list : ["current"];
2109
+ }
2110
+ function toSet(v) {
2111
+ if (typeof v !== "string")
2112
+ return new Set;
2113
+ return new Set(v.split(",").map((s) => s.trim()).filter(Boolean));
2114
+ }
2115
+ function toRoots(v) {
2116
+ if (typeof v !== "string")
2117
+ return;
2118
+ const arr = v.split(",").map((s) => s.trim()).filter(Boolean);
2119
+ return arr.length ? arr : undefined;
2120
+ }
2121
+ function readManifestFile(path) {
2122
+ if (!existsSync6(path))
2123
+ throw new Error(`manifest file not found: ${path}`);
2124
+ try {
2125
+ const raw = readFileSync(path, "utf8").replace(/^\uFEFF/, "");
2126
+ return JSON.parse(raw);
2127
+ } catch (e) {
2128
+ throw new Error(`manifest parse failed (${path}): ${e.message}`);
2129
+ }
2130
+ }
2131
+ async function resolveManifest(explicitPath, shells, disabled, label, roots) {
2132
+ if (explicitPath)
2133
+ return { manifest: readManifestFile(explicitPath), source: explicitPath };
2134
+ if (existsSync6(DEFAULT_MANIFEST))
2135
+ return { manifest: readManifestFile(DEFAULT_MANIFEST), source: DEFAULT_MANIFEST };
2136
+ const manifest = await runScan(shells, disabled, { projectRoots: roots });
2137
+ return { manifest, source: label + "(live scan)" };
2138
+ }
2139
+ async function cmdScan(opts, pos) {
2140
+ const shells = toShells(opts.shell);
2141
+ const disabled = toSet(opts.disable);
2142
+ const roots = toRoots(opts["scan-root"]);
2143
+ const manifest = await runScan(shells, disabled, {
2144
+ projectRoots: roots,
2145
+ silent: opts.quiet === true,
2146
+ checkOutdated: opts.outdated === true,
2147
+ wslDeep: opts["wsl-deep"] === true,
2148
+ auditDeps: opts["audit-deps"] === true,
2149
+ healthNetwork: opts["health-network"] === true
2150
+ });
2151
+ if (opts.json) {
2152
+ emitText(JSON.stringify(manifest, null, 2), opts.bom === true);
2153
+ } else if (opts.report) {
2154
+ emitText(clean(renderMarkdownLite(manifest)), opts.bom === true);
2155
+ } else {
2156
+ renderPrettyFromManifest(manifest);
2157
+ if (opts["no-manifest"] !== true) {
2158
+ try {
2159
+ mkdirSync(join7(DEFAULT_MANIFEST, ".."), { recursive: true });
2160
+ writeFileSync(DEFAULT_MANIFEST, JSON.stringify(manifest, null, 2));
2161
+ console.log(` ${"\x1B[2m"}manifest → ${DEFAULT_MANIFEST}${"\x1B[0m"}`);
2162
+ } catch {}
2163
+ }
2164
+ }
2165
+ if (opts.ci) {
2166
+ const hasError = (manifest.checks || []).some((c) => c.Severity === "error");
2167
+ if (hasError)
2168
+ return 1;
2169
+ const hasWarn = (manifest.checks || []).some((c) => c.Severity === "warn");
2170
+ if (hasWarn)
2171
+ return 2;
2172
+ return 0;
2173
+ }
2174
+ return 0;
2175
+ }
2176
+ async function cmdQuery(opts, pos) {
2177
+ const selector = pos[1];
2178
+ const shells = toShells(opts.shell);
2179
+ const disabled = toSet(opts.disable);
2180
+ const { manifest, source } = await resolveManifest(opts.manifest, shells, disabled, "query", toRoots(opts["scan-root"]));
2181
+ const result = queryManifest(manifest, {
2182
+ selector,
2183
+ issues: opts.issues === true,
2184
+ conflicts: opts.conflicts === true,
2185
+ offPath: opts["off-path"] === true || opts.offpath === true,
2186
+ kind: opts.kind,
2187
+ where: opts.where
2188
+ });
2189
+ if (opts.json) {
2190
+ emitText(JSON.stringify({ source, ...result }, null, 2), opts.bom === true);
2191
+ return 0;
2192
+ }
2193
+ console.log(`\x1B[1m\uD83D\uDD0D query:\x1B[0m ${selector || "(all)"} \x1B[2m(source: ${source})\x1B[0m`);
2194
+ for (const g of result.groups) {
2195
+ console.log(` \x1B[36m${catLabel(g.id, g.category)}\x1B[0m ${g.id} \x1B[2m(${g.installs.length})\x1B[0m`);
2196
+ for (const inst of g.installs) {
2197
+ const loc = locationOf(inst);
2198
+ const tag = loc === "PATH" ? "\x1B[32m[onPath]\x1B[0m" : loc === "env" ? `\x1B[36m[env: ${inst.env}]\x1B[0m` : "\x1B[33m[off-path]\x1B[0m";
2199
+ console.log(` ${inst.path} \x1B[2m${inst.version || "?"}\x1B[0m ${tag}`);
2200
+ }
2201
+ }
2202
+ if (result.conflicts) {
2203
+ const c = result.conflicts;
2204
+ console.log(` \x1B[1mconflicts\x1B[0m multiVersion=${c.multiVersionTools?.length || 0} path=${c.pathConflicts?.length || 0} ghost=${c.ghostBinaries?.length || 0}`);
2205
+ }
2206
+ const allChecks = result.checks || [];
2207
+ const issues = allChecks.filter((c) => bucketOf(c.Severity) === "issue");
2208
+ const infos = allChecks.filter((c) => bucketOf(c.Severity) === "info");
2209
+ const oks = allChecks.filter((c) => bucketOf(c.Severity) === "ok");
2210
+ if (issues.length) {
2211
+ console.log(` \x1B[1missues (${issues.length})\x1B[0m`);
2212
+ for (const c of issues) {
2213
+ const sev = sevColor(c.Severity);
2214
+ console.log(` ${sev}${c.Severity}\x1B[0m ${c.ID} ${c.Label}`);
2215
+ }
2216
+ }
2217
+ if (infos.length) {
2218
+ console.log(` \x1B[1minfo (${infos.length})\x1B[0m`);
2219
+ for (const c of infos)
2220
+ console.log(` ${sevColor(c.Severity)}${c.Severity}\x1B[0m ${c.ID} ${c.Label}`);
2221
+ }
2222
+ if (oks.length) {
2223
+ console.log(` \x1B[32m✓ ${oks.length} checks passed\x1B[0m`);
2224
+ }
2225
+ if (!result.groups.length && !result.checks.length && !result.conflicts) {
2226
+ console.log(" (no match)");
2227
+ }
2228
+ return 0;
2229
+ }
2230
+ async function cmdDevdiff(opts, pos) {
2231
+ const shells = toShells(opts.shell);
2232
+ const disabled = toSet(opts.disable);
2233
+ const files = pos.slice(1).filter((p) => !p.startsWith("-"));
2234
+ let A, labelA;
2235
+ let B, labelB;
2236
+ if (files.length >= 2) {
2237
+ A = readManifestFile(files[0]);
2238
+ labelA = files[0];
2239
+ B = readManifestFile(files[1]);
2240
+ labelB = files[1];
2241
+ } else if (files.length === 1) {
2242
+ const before = await resolveManifest(opts.manifest, shells, disabled, "devdiff-before", toRoots(opts["scan-root"]));
2243
+ A = before.manifest;
2244
+ labelA = before.source;
2245
+ B = readManifestFile(files[0]);
2246
+ labelB = files[0];
2247
+ } else {
2248
+ const before = await resolveManifest(opts.manifest, shells, disabled, "devdiff-before", toRoots(opts["scan-root"]));
2249
+ A = before.manifest;
2250
+ labelA = before.source;
2251
+ B = await runScan(shells, disabled, { projectRoots: toRoots(opts["scan-root"]) });
2252
+ labelB = "current live scan";
2253
+ }
2254
+ const diff = diffManifests(A, B);
2255
+ if (opts.json) {
2256
+ emitText(JSON.stringify({ before: labelA, after: labelB, ...diff }, null, 2), opts.bom === true);
2257
+ return 0;
2258
+ }
2259
+ console.log(`\x1B[1m⚖ devdiff\x1B[0m before: \x1B[2m${labelA}\x1B[0m → after: \x1B[2m${labelB}\x1B[0m`);
2260
+ for (const t of diff.tools) {
2261
+ for (const inst of t.added)
2262
+ console.log(` \x1B[32m+ ${t.category} ${t.id}\x1B[0m ${inst.path} ${inst.version || "?"}`);
2263
+ for (const inst of t.removed)
2264
+ console.log(` \x1B[31m- ${t.category} ${t.id}\x1B[0m ${inst.path} ${inst.version || "?"}`);
2265
+ for (const vc of t.versionChanged)
2266
+ console.log(` \x1B[33m~ ${t.category} ${t.id}\x1B[0m ${vc.path} ${vc.from || "?"} → ${vc.to || "?"}`);
2267
+ }
2268
+ for (const iss of diff.issues) {
2269
+ for (const c of iss.added)
2270
+ console.log(` \x1B[31m+ issue\x1B[0m ${sevColor(c.Severity)}${c.Severity}\x1B[0m ${c.ID} ${c.Label}`);
2271
+ for (const c of iss.resolved)
2272
+ console.log(` \x1B[32m- issue\x1B[0m ${sevColor(c.Severity)}${c.Severity}\x1B[0m ${c.ID} ${c.Label}`);
2273
+ }
2274
+ const s = diff.summary;
2275
+ console.log(` \x1B[2m→ +${s.installsAdded} installs / -${s.installsRemoved} installs / ~${s.versionChanges} version changes / +${s.issuesAdded} issues / -${s.issuesResolved} resolved\x1B[0m`);
2276
+ if (!diff.tools.length && !diff.issues.length)
2277
+ console.log(" \x1B[32mno diff\x1B[0m");
2278
+ return 0;
2279
+ }
2280
+ function sevColor(sev) {
2281
+ switch (sev) {
2282
+ case "error":
2283
+ return "\x1B[31m";
2284
+ case "warn":
2285
+ return "\x1B[33m";
2286
+ case "info":
2287
+ return "\x1B[36m";
2288
+ case "ok":
2289
+ return "\x1B[32m";
2290
+ default:
2291
+ return "\x1B[0m";
2292
+ }
2293
+ }
2294
+ function bucketOf(sev) {
2295
+ if (sev === "error" || sev === "warn" || sev === "missing")
2296
+ return "issue";
2297
+ if (sev === "info")
2298
+ return "info";
2299
+ return "ok";
2300
+ }
2301
+ var CATEGORY = {
2302
+ node: "runtime",
2303
+ python: "runtime",
2304
+ ruby: "runtime",
2305
+ java: "runtime",
2306
+ bun: "runtime",
2307
+ go: "toolchain",
2308
+ rustc: "toolchain",
2309
+ npm: "pkg",
2310
+ pip: "pkg",
2311
+ pip3: "pkg",
2312
+ conda: "pkg",
2313
+ cargo: "pkg",
2314
+ brew: "pkg",
2315
+ winget: "pkg",
2316
+ scoop: "pkg",
2317
+ poetry: "pkg",
2318
+ yarn: "pkg",
2319
+ pnpm: "pkg",
2320
+ nvm: "vermgr",
2321
+ pyenv: "vermgr",
2322
+ git: "tool",
2323
+ docker: "tool",
2324
+ kubectl: "tool"
2325
+ };
2326
+ function catLabel(id, fallbackKey) {
2327
+ if (CATEGORY[id])
2328
+ return CATEGORY[id];
2329
+ if (fallbackKey === "runtimes")
2330
+ return "runtime";
2331
+ if (fallbackKey === "packageManagers")
2332
+ return "pkg";
2333
+ if (fallbackKey === "ides")
2334
+ return "ide";
2335
+ return fallbackKey;
2336
+ }
2337
+ function renderPrettyFromManifest(m) {
2338
+ const sys = m.system || {};
2339
+ console.log(`\x1B[1mdevmap\x1B[0m v${m.meta?.version || VERSION} \x1B[2m${sys.hostname || "?"}\x1B[0m ${sys.platform}/${sys.arch}`);
2340
+ console.log(` shell=${sys.shell || "?"} pathDirs=${sys.pathDirCount ?? "?"}`);
2341
+ console.log(" " + "─".repeat(40));
2342
+ for (const key of ["runtimes", "packageManagers", "ides"]) {
2343
+ for (const g of m[key] || []) {
2344
+ console.log(` \x1B[36m${catLabel(g.id, key)}\x1B[0m ${g.id} \x1B[2m(${g.installs.length})\x1B[0m`);
2345
+ for (const inst of g.installs.slice(0, 6)) {
2346
+ const tag = inst.onPath ? "" : inst.env ? ` \x1B[36m[env: ${inst.env}]\x1B[0m` : " \x1B[33m[off-path]\x1B[0m";
2347
+ console.log(` ${inst.path} \x1B[2m${inst.version || "?"}\x1B[0m${tag}`);
2348
+ }
2349
+ }
2350
+ }
2351
+ const allChecks = m.checks || [];
2352
+ const issues = allChecks.filter((c) => bucketOf(c.Severity) === "issue");
2353
+ const infos = allChecks.filter((c) => bucketOf(c.Severity) === "info");
2354
+ const oks = allChecks.filter((c) => bucketOf(c.Severity) === "ok");
2355
+ renderEnvironments(m.environments);
2356
+ if (issues.length || infos.length || oks.length) {
2357
+ console.log(" " + "─".repeat(40));
2358
+ if (issues.length) {
2359
+ console.log(` \x1B[1missues (${issues.length})\x1B[0m`);
2360
+ for (const c of issues)
2361
+ console.log(` ${sevColor(c.Severity)}${c.Severity}\x1B[0m ${c.ID} ${c.Label}`);
2362
+ }
2363
+ if (infos.length) {
2364
+ console.log(` \x1B[1minfo (${infos.length})\x1B[0m`);
2365
+ for (const c of infos)
2366
+ console.log(` ${sevColor(c.Severity)}${c.Severity}\x1B[0m ${c.ID} ${c.Label}`);
2367
+ }
2368
+ if (oks.length) {
2369
+ console.log(` \x1B[32m✓ ${oks.length} checks passed\x1B[0m`);
2370
+ }
2371
+ }
2372
+ }
2373
+ var plur = (n, one, many = one + "s") => `${n} ${n === 1 ? one : many}`;
2374
+ function renderEnvironments(env) {
2375
+ if (!env)
2376
+ return;
2377
+ const rule = " " + "─".repeat(40);
2378
+ if (env.vcpkg?.installed) {
2379
+ console.log(rule);
2380
+ console.log(` \x1B[1mvcpkg\x1B[0m ${plur(env.vcpkg.packages.length, "package")} (\x1B[33m${plur(env.vcpkg.installs.length, "vcpkg copy", "vcpkg copies")}\x1B[0m)`);
2381
+ for (const p of env.vcpkg.packages.slice(0, 6))
2382
+ console.log(` ${p.name}@${p.version} \x1B[2m${p.triplet}\x1B[0m`);
2383
+ }
2384
+ if (env.pip?.perInterpreter?.length) {
2385
+ console.log(rule);
2386
+ console.log(` \x1B[1mpip\x1B[0m ${plur(env.pip.perInterpreter.length, "interpreter")}`);
2387
+ for (const pi of env.pip.perInterpreter) {
2388
+ const short = pi.pythonPath.split(/[\\/]/).slice(-2).join("/").replace(/\.exe$/i, "");
2389
+ console.log(` ${short} \x1B[2m${plur(pi.packages.length, "package")}${pi.outdated.length ? " · " + pi.outdated.length + " upgradable" : ""}\x1B[0m`);
2390
+ }
2391
+ }
2392
+ if (env.apt?.available) {
2393
+ console.log(rule);
2394
+ console.log(` \x1B[1mapt\x1B[0m ${plur(env.apt.count, "package")} · ${env.apt.upgradable} upgradable`);
2395
+ }
2396
+ if (env.condaEnvs?.envs?.length) {
2397
+ console.log(rule);
2398
+ console.log(` \x1B[1mconda envs\x1B[0m (${env.condaEnvs.envs.length})`);
2399
+ for (const e of env.condaEnvs.envs)
2400
+ console.log(` ${e.active ? "*" : " "} ${e.name} \x1B[2m${plur(e.packages.length, "package")}${e.outdated.length ? " · " + e.outdated.length + " upgradable" : ""}\x1B[0m`);
2401
+ }
2402
+ if (env.docker?.installed) {
2403
+ console.log(rule);
2404
+ console.log(` \x1B[1mdocker\x1B[0m daemon=${env.docker.daemonRunning ? "\x1B[32mrunning\x1B[0m" : "\x1B[31mnot running\x1B[0m"} images=${env.docker.images} containers=${env.docker.containers}`);
2405
+ }
2406
+ if (env.wsl?.available && env.wsl.distros.length) {
2407
+ console.log(rule);
2408
+ console.log(` \x1B[1mWSL\x1B[0m (${plur(env.wsl.distros.length, "distro")})`);
2409
+ for (const d of env.wsl.distros) {
2410
+ const running = /running/i.test(d.state);
2411
+ const state = running ? `\x1B[32m${d.state}\x1B[0m` : `\x1B[2m${d.state}\x1B[0m`;
2412
+ console.log(` ${d.default ? "*" : " "} ${d.name} ${state} \x1B[2m· WSL${d.version}\x1B[0m`);
2413
+ if (d.basePath)
2414
+ console.log(` \x1B[2mlocation: ${d.basePath}\x1B[0m`);
2415
+ if (d.probed && d.tools) {
2416
+ const have = Object.entries(d.tools).filter(([, v]) => v);
2417
+ console.log(have.length ? ` \x1B[2mtools:\x1B[0m ${have.map(([k, v]) => `${k}=${v}`).join(" ")}` : ` \x1B[2mtools: (not detected)\x1B[0m`);
2418
+ } else {
2419
+ console.log(` \x1B[2mtools: not probed (distro not running; add --wsl-deep to probe)\x1B[0m`);
2420
+ }
2421
+ }
2422
+ }
2423
+ if (env.vsShells?.shells?.length) {
2424
+ console.log(rule);
2425
+ console.log(` \x1B[1mVisual Studio dev shells\x1B[0m (${env.vsShells.shells.length})`);
2426
+ for (const s of env.vsShells.shells) {
2427
+ const present = Object.entries(s.tools).filter(([, v]) => v).map(([k]) => k);
2428
+ const tag = present.length ? `\x1B[32m${present.join("/")}\x1B[0m` : `\x1B[33mno C++ toolchain\x1B[0m`;
2429
+ console.log(` ${s.name} \x1B[2m[${s.kind}/${s.arch}]\x1B[0m ${tag}`);
2430
+ }
2431
+ }
2432
+ if (env.projectEnvs?.envs?.length) {
2433
+ console.log(rule);
2434
+ console.log(` \x1B[1mproject-local environments\x1B[0m (${env.projectEnvs.envs.length})`);
2435
+ for (const e of env.projectEnvs.envs.slice(0, 12))
2436
+ console.log(` ${e.project} \x1B[2m${e.type}\x1B[0m ${e.path}`);
2437
+ }
2438
+ }
2439
+ function renderMarkdownLite(m) {
2440
+ const lines = [`# devmap report`, "", `> host: ${m.system?.hostname} · ${m.system?.platform}/${m.system?.arch} · pathDirs=${m.system?.pathDirCount}`, ""];
2441
+ for (const key of ["runtimes", "packageManagers", "ides"]) {
2442
+ lines.push(`## ${key}`);
2443
+ for (const g of m[key] || []) {
2444
+ lines.push(`- **${g.id}** _${catLabel(g.id, key)}_ (${g.installs.length})`);
2445
+ for (const inst of g.installs) {
2446
+ const tag = locationTag(inst);
2447
+ lines.push(` - ${inst.path} — ${inst.version || "?"}${tag ? ` (${tag})` : ""}`);
2448
+ }
2449
+ }
2450
+ lines.push("");
2451
+ }
2452
+ const allChecks = m.checks || [];
2453
+ const issues = allChecks.filter((c) => bucketOf(c.Severity) === "issue");
2454
+ const infos = allChecks.filter((c) => bucketOf(c.Severity) === "info");
2455
+ const oks = allChecks.filter((c) => bucketOf(c.Severity) === "ok");
2456
+ if (issues.length) {
2457
+ lines.push(`## issues (${issues.length})`);
2458
+ for (const c of issues)
2459
+ lines.push(`- [${c.Severity}] \`${c.ID}\` ${c.Label} — ${c.Detail}`);
2460
+ }
2461
+ if (infos.length) {
2462
+ lines.push(`## info (${infos.length})`);
2463
+ for (const c of infos)
2464
+ lines.push(`- [${c.Severity}] \`${c.ID}\` ${c.Label} — ${c.Detail}`);
2465
+ }
2466
+ if (oks.length) {
2467
+ lines.push(`## checks passed (${oks.length})`);
2468
+ for (const c of oks)
2469
+ lines.push(`- [${c.Severity}] \`${c.ID}\` ${c.Label}`);
2470
+ }
2471
+ const env = m.environments || {};
2472
+ const envLines = [];
2473
+ if (env.vcpkg?.installed)
2474
+ envLines.push(`- vcpkg: ${plur(env.vcpkg.packages.length, "package")} / ${plur(env.vcpkg.installs.length, "vcpkg copy", "vcpkg copies")}`);
2475
+ if (env.pip?.perInterpreter?.length)
2476
+ envLines.push(`- pip: ${plur(env.pip.perInterpreter.length, "interpreter")} / ${plur(env.pip.perInterpreter.reduce((a, p) => a + p.packages.length, 0), "package")}`);
2477
+ if (env.apt?.available)
2478
+ envLines.push(`- apt: ${plur(env.apt.count, "package")} / ${env.apt.upgradable} upgradable`);
2479
+ if (env.condaEnvs?.envs?.length)
2480
+ envLines.push(`- conda envs: ${env.condaEnvs.envs.length}`);
2481
+ if (env.docker?.installed)
2482
+ envLines.push(`- docker: daemon=${env.docker.daemonRunning} images=${env.docker.images} containers=${env.docker.containers}`);
2483
+ if (env.wsl?.available)
2484
+ envLines.push(`- WSL: ${plur(env.wsl.distros?.length || 0, "distro")}`);
2485
+ if (env.vsShells?.shells?.length)
2486
+ envLines.push(`- Visual Studio dev shells: ${env.vsShells.shells.length}`);
2487
+ if (env.projectEnvs?.envs?.length)
2488
+ envLines.push(`- project-local environments: ${env.projectEnvs.envs.length}`);
2489
+ if (envLines.length) {
2490
+ lines.push("", `## environments (Phase 2)`);
2491
+ lines.push(...envLines);
2492
+ }
2493
+ return lines.join(`
2494
+ `);
2495
+ }
2496
+ function printHelp() {
2497
+ console.log(`devmap v${VERSION} — machine dev-environment source of truth
2498
+
2499
+ Usage:
2500
+ devmap [scan] [options] scan and render nicely (scan is the DEFAULT — 'devmap' alone is enough)
2501
+ devmap query <tool> query a generated manifest (devmap.json)
2502
+ devmap devdiff [a] [b] compare two manifests (environment drift / cross-machine consistency)
2503
+
2504
+ Tip: every 'devmap scan ...' also works as just 'devmap ...' (the 'scan' word is optional).
2505
+
2506
+ scan options:
2507
+ --json emit machine-readable JSON (devmap.json schema)
2508
+ --report emit full markdown report
2509
+ --no-manifest do not persist the manifest to disk
2510
+ --manifest <p> specify the manifest location (default ~/.devmap/devmap.json)
2511
+ --disable <r1,r2> disable check rules (prefix match, e.g. path.duplicate / pip.outdated)
2512
+ --shell <list> which shell views to reconstruct (current|powershell|gitbash|wsl|all)
2513
+ --scan-root <dir> project-local environment scan root (comma-separated; default cwd). e.g. --scan-root C:\\src
2514
+ --outdated check pip/conda upgradable packages online (off by default: requires PyPI access and slows the scan)
2515
+ --wsl-deep probe inside stopped WSL distros (default probes only running ones to avoid waking distros)
2516
+ --ci CI gate: clean → exit 0; any warn → exit 2; any error → exit 1
2517
+ --audit-deps dependency CVE audit via OSV-Scanner/pip-audit/govulncheck/Trivy (opt-in, uses network)
2518
+ --health-network run network health probes too (npm doctor / rustup check; off by default to stay offline-fast)
2519
+ --quiet suppress scan progress feedback (for scripts/CI)
2520
+ --bom prepend UTF-8 BOM for viewing in Notepad etc. (only on redirect/pipe)
2521
+ --version / --help
2522
+
2523
+ Phase 2 environment layer (covered by this scan):
2524
+ vcpkg pkgs / per-python-interpreter pip packages (incl. upgradable) / apt packages (Linux) / conda env packages /
2525
+ docker (binary+daemon+images/containers) / WSL distros / Visual Studio dev shells / project-local environments (venv, node_modules)
2526
+
2527
+ Encoding note: output is plain UTF-8. Opening BOM-less UTF-8 in Windows Notepad may show mojibake,
2528
+ use VS Code / Windows Terminal cat / an IDE to open it; or add --bom then redirect.
2529
+ When reading a BOM file in Python use open(p, encoding="utf-8-sig").
2530
+
2531
+ query options:
2532
+ <tool> tool id substring (e.g. node), or "category.tool" dotted form (e.g. runtime.node / pkg.npm)
2533
+ --issues list issues only
2534
+ --conflicts list conflicts only (multiVersion/path/ghost)
2535
+ --off-path list isolated off-PATH installs only (version-manager envs excluded)
2536
+ --kind <runtime|packageManager|ide> filter by category
2537
+ --where <substr> filter by install-path substring
2538
+ --manifest <p> manifest to query (default ~/.devmap/devmap.json; live scan if absent)
2539
+ --json emit JSON
2540
+
2541
+ devdiff options:
2542
+ (no args) compare "stored manifest" vs "current live scan"
2543
+ <file> compare "stored/specified before" vs <file>
2544
+ <a> <b> compare manifests a and b
2545
+ --json emit JSON
2546
+ `);
2547
+ }
2548
+ async function main() {
2549
+ const { pos, opts } = tokenize(process.argv.slice(2));
2550
+ if (opts.help || opts.h) {
2551
+ printHelp();
2552
+ process.exit(0);
2553
+ }
2554
+ if (opts.version || opts.v) {
2555
+ console.log(VERSION);
2556
+ process.exit(0);
2557
+ }
2558
+ const sub = pos[0] || "scan";
2559
+ let code = 0;
2560
+ try {
2561
+ if (sub === "query")
2562
+ code = await cmdQuery(opts, pos);
2563
+ else if (sub === "devdiff")
2564
+ code = await cmdDevdiff(opts, pos);
2565
+ else
2566
+ code = await cmdScan(opts, pos);
2567
+ } catch (e) {
2568
+ console.error(`\x1B[31merror:\x1B[0m ${e.message}`);
2569
+ process.exit(2);
2570
+ }
2571
+ process.exit(code);
2572
+ }
2573
+ main();