@vibgrate/cli 0.1.3 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,13 +56,19 @@ That's it. You'll see a full drift report in seconds.
56
56
  ## What You Get
57
57
 
58
58
  ```
59
+ ╭───╮➜
60
+ ╭┤◉ ◉├╮ V I B G R A T E
61
+ ╰┤───├╯ Drift Intelligence Engine v1.x.x
62
+ ╰───╯
63
+
59
64
  ╔══════════════════════════════════════════╗
60
65
  ║ Vibgrate Drift Report ║
61
66
  ╚══════════════════════════════════════════╝
62
67
 
63
68
  Drift Score: 72/100
64
- Risk Level: Low
69
+ Risk Level: LOW
65
70
  Projects: 3
71
+ VCS: git main a1b2c3d
66
72
 
67
73
  Score Breakdown
68
74
  Runtime: ████████████████████ 100
@@ -77,9 +83,61 @@ That's it. You'll see a full drift report in seconds.
77
83
  Dependencies:
78
84
  42 current 8 1-behind 3 2+ behind
79
85
 
80
- Findings
86
+ ── web-app (node) src/web
87
+ Runtime: 20.11.0 (current)
88
+ Frameworks:
89
+ React: 18.2.0 → 19.0.0 (1 behind)
90
+ Dependencies:
91
+ 31 current 5 1-behind 2 2+ behind
92
+
93
+ Tech Stack
94
+ Frontend: React, Tailwind CSS
95
+ Bundlers: Vite
96
+ Testing: Vitest, Playwright
97
+ Lint & Format: ESLint, Prettier
98
+
99
+ Services & Integrations
100
+ Cloud: AWS SDK v3
101
+ Databases: PostgreSQL
102
+
103
+ TypeScript
104
+ v5.4.2 · strict ✔ · ESM · target: ES2022
105
+
106
+ Build & Deploy
107
+ CI: GitHub Actions
108
+ Docker: 2 Dockerfiles (node:20-alpine)
109
+ Package Managers: pnpm
110
+
111
+ Security Posture
112
+ Lockfile ✔ · .env ✔ · node_modules ✔
113
+
114
+ Dependency Graph
115
+ pnpm-lock.yaml: 312 unique, 487 installed
116
+ 5 duplicated packages
117
+
118
+ Findings (2 warnings, 1 note)
81
119
  ⚠ Framework "NestJS" is 1 major version(s) behind
120
+ framework/outdated in src/api/package.json
82
121
  ⚠ 12% of dependencies are 2+ major versions behind
122
+ dependency/outdated in src/api/package.json
123
+ ℹ TypeScript target is ES2022
124
+ ts/target in tsconfig.json
125
+
126
+ ╔══════════════════════════════════════════╗
127
+ ║ Top Priority Actions ║
128
+ ╚══════════════════════════════════════════╝
129
+
130
+ 1. Upgrade NestJS 10.3.0 → 11.0.0 in my-api
131
+ 1 major version behind. Major framework drift increases
132
+ breaking change risk and blocks access to security fixes.
133
+ Impact: +5–15 points (framework score)
134
+
135
+ 2. Reduce dependency rot in my-api (42% severely outdated)
136
+ 3 of 53 dependencies are 2+ majors behind. Run `npm outdated`
137
+ and prioritise packages with known CVEs.
138
+ Impact: +5–10 points (dependency score)
139
+
140
+ Scanned at 2026-02-16T00:00:00.000Z · 1.2s · 48 files scanned
83
141
  ```
84
142
 
85
143
  ---
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  baselineCommand,
3
3
  runBaseline
4
- } from "./chunk-3X3ZMVHI.js";
5
- import "./chunk-VXEZ7APL.js";
4
+ } from "./chunk-NTRKEIKP.js";
5
+ import "./chunk-VMNBKARQ.js";
6
6
  export {
7
7
  baselineCommand,
8
8
  runBaseline
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  runScan,
3
3
  writeJsonFile
4
- } from "./chunk-VXEZ7APL.js";
4
+ } from "./chunk-VMNBKARQ.js";
5
5
 
6
6
  // src/commands/baseline.ts
7
7
  import * as path from "path";
@@ -95,9 +95,9 @@ function clamp(val, min, max) {
95
95
  return Math.min(max, Math.max(min, val));
96
96
  }
97
97
  function runtimeScore(projects) {
98
- if (projects.length === 0) return 100;
98
+ if (projects.length === 0) return null;
99
99
  const lags = projects.map((p) => p.runtimeMajorsBehind).filter((v) => v !== void 0);
100
- if (lags.length === 0) return 100;
100
+ if (lags.length === 0) return null;
101
101
  const maxLag = Math.max(...lags);
102
102
  if (maxLag === 0) return 100;
103
103
  if (maxLag === 1) return 80;
@@ -107,9 +107,9 @@ function runtimeScore(projects) {
107
107
  }
108
108
  function frameworkScore(projects) {
109
109
  const allFrameworks = projects.flatMap((p) => p.frameworks);
110
- if (allFrameworks.length === 0) return 100;
110
+ if (allFrameworks.length === 0) return null;
111
111
  const lags = allFrameworks.map((f) => f.majorsBehind).filter((v) => v !== null);
112
- if (lags.length === 0) return 100;
112
+ if (lags.length === 0) return null;
113
113
  const maxLag = Math.max(...lags);
114
114
  const avgLag = lags.reduce((a, b) => a + b, 0) / lags.length;
115
115
  const maxPenalty = Math.min(maxLag * 20, 100);
@@ -128,13 +128,15 @@ function dependencyScore(projects) {
128
128
  totalUnknown += p.dependencyAgeBuckets.unknown;
129
129
  }
130
130
  const total = totalCurrent + totalOne + totalTwo;
131
- if (total === 0) return 100;
131
+ if (total === 0) return null;
132
132
  const currentPct = totalCurrent / total;
133
133
  const onePct = totalOne / total;
134
134
  const twoPct = totalTwo / total;
135
135
  return clamp(Math.round(currentPct * 100 - onePct * 10 - twoPct * 40), 0, 100);
136
136
  }
137
137
  function eolScore(projects) {
138
+ const hasRuntimeData = projects.some((p) => p.runtimeMajorsBehind !== void 0);
139
+ if (!hasRuntimeData) return null;
138
140
  let score = 100;
139
141
  for (const p of projects) {
140
142
  if (p.type === "node" && p.runtimeMajorsBehind !== void 0) {
@@ -155,20 +157,50 @@ function computeDriftScore(projects) {
155
157
  const fs5 = frameworkScore(projects);
156
158
  const ds = dependencyScore(projects);
157
159
  const es = eolScore(projects);
158
- const score = Math.round(rs * 0.25 + fs5 * 0.25 + ds * 0.3 + es * 0.2);
160
+ const components = [
161
+ { score: rs, weight: 0.25 },
162
+ { score: fs5, weight: 0.25 },
163
+ { score: ds, weight: 0.3 },
164
+ { score: es, weight: 0.2 }
165
+ ];
166
+ const active = components.filter((c) => c.score !== null);
167
+ if (active.length === 0) {
168
+ return {
169
+ score: 100,
170
+ riskLevel: "low",
171
+ components: {
172
+ runtimeScore: rs ?? 100,
173
+ frameworkScore: fs5 ?? 100,
174
+ dependencyScore: ds ?? 100,
175
+ eolScore: es ?? 100
176
+ }
177
+ };
178
+ }
179
+ const totalActiveWeight = active.reduce((sum, c) => sum + c.weight, 0);
180
+ let score = 0;
181
+ for (const c of active) {
182
+ score += c.score * (c.weight / totalActiveWeight);
183
+ }
184
+ score = Math.round(score);
159
185
  let riskLevel;
160
186
  if (score >= 70) riskLevel = "low";
161
187
  else if (score >= 40) riskLevel = "moderate";
162
188
  else riskLevel = "high";
189
+ const measured = [];
190
+ if (rs !== null) measured.push("runtime");
191
+ if (fs5 !== null) measured.push("framework");
192
+ if (ds !== null) measured.push("dependency");
193
+ if (es !== null) measured.push("eol");
163
194
  return {
164
195
  score,
165
196
  riskLevel,
166
197
  components: {
167
- runtimeScore: rs,
168
- frameworkScore: fs5,
169
- dependencyScore: ds,
170
- eolScore: es
171
- }
198
+ runtimeScore: rs ?? 100,
199
+ frameworkScore: fs5 ?? 100,
200
+ dependencyScore: ds ?? 100,
201
+ eolScore: es ?? 100
202
+ },
203
+ measured
172
204
  };
173
205
  }
174
206
  function generateFindings(projects, config) {
@@ -243,11 +275,22 @@ function generateFindings(projects, config) {
243
275
  return findings;
244
276
  }
245
277
 
278
+ // src/version.ts
279
+ import { createRequire } from "module";
280
+ var require2 = createRequire(import.meta.url);
281
+ var pkg = require2("../package.json");
282
+ var VERSION = pkg.version;
283
+
246
284
  // src/formatters/text.ts
247
285
  import chalk from "chalk";
248
286
  function formatText(artifact) {
249
287
  const lines = [];
250
288
  lines.push("");
289
+ lines.push(chalk.cyan(" \u256D\u2500\u2500\u2500\u256E") + chalk.greenBright("\u279C"));
290
+ lines.push(chalk.cyan(" \u256D\u2524") + chalk.greenBright("\u25C9 \u25C9") + chalk.cyan("\u251C\u256E") + " " + chalk.bold.white("V I B G R A T E"));
291
+ lines.push(chalk.cyan(" \u2570\u2524") + chalk.dim("\u2500\u2500\u2500") + chalk.cyan("\u251C\u256F") + " " + chalk.dim(`Drift Intelligence Engine v${VERSION}`));
292
+ lines.push(chalk.cyan(" \u2570\u2500\u2500\u2500\u256F"));
293
+ lines.push("");
251
294
  lines.push(chalk.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
252
295
  lines.push(chalk.bold.cyan("\u2551 Vibgrate Drift Report \u2551"));
253
296
  lines.push(chalk.bold.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
@@ -263,11 +306,12 @@ function formatText(artifact) {
263
306
  lines.push(chalk.bold(" VCS: ") + vcsParts.join(" "));
264
307
  }
265
308
  lines.push("");
309
+ const m = new Set(artifact.drift.measured ?? ["runtime", "framework", "dependency", "eol"]);
266
310
  lines.push(chalk.bold.underline(" Score Breakdown"));
267
- lines.push(` Runtime: ${scoreBar(artifact.drift.components.runtimeScore)}`);
268
- lines.push(` Frameworks: ${scoreBar(artifact.drift.components.frameworkScore)}`);
269
- lines.push(` Dependencies: ${scoreBar(artifact.drift.components.dependencyScore)}`);
270
- lines.push(` EOL Risk: ${scoreBar(artifact.drift.components.eolScore)}`);
311
+ lines.push(` Runtime: ${m.has("runtime") ? scoreBar(artifact.drift.components.runtimeScore) : chalk.dim("n/a")}`);
312
+ lines.push(` Frameworks: ${m.has("framework") ? scoreBar(artifact.drift.components.frameworkScore) : chalk.dim("n/a")}`);
313
+ lines.push(` Dependencies: ${m.has("dependency") ? scoreBar(artifact.drift.components.dependencyScore) : chalk.dim("n/a")}`);
314
+ lines.push(` EOL Risk: ${m.has("eol") ? scoreBar(artifact.drift.components.eolScore) : chalk.dim("n/a")}`);
271
315
  lines.push("");
272
316
  for (const project of artifact.projects) {
273
317
  lines.push(chalk.bold(` \u2500\u2500 ${project.name} `) + chalk.dim(`(${project.type}) ${project.path}`));
@@ -293,8 +337,24 @@ function formatText(artifact) {
293
337
  }
294
338
  lines.push("");
295
339
  }
340
+ if (artifact.delta !== void 0) {
341
+ const deltaStr = artifact.delta > 0 ? chalk.green(`+${artifact.delta}`) : artifact.delta < 0 ? chalk.red(`${artifact.delta}`) : chalk.dim("0");
342
+ lines.push(chalk.bold(" Drift Delta: ") + deltaStr + " (vs baseline)");
343
+ lines.push("");
344
+ }
345
+ if (artifact.extended) {
346
+ lines.push(...formatExtended(artifact.extended));
347
+ }
296
348
  if (artifact.findings.length > 0) {
297
- lines.push(chalk.bold.underline(" Findings"));
349
+ const errors = artifact.findings.filter((f) => f.level === "error");
350
+ const warnings = artifact.findings.filter((f) => f.level === "warning");
351
+ const notes = artifact.findings.filter((f) => f.level === "note");
352
+ const summary = [
353
+ errors.length > 0 ? chalk.red(`${errors.length} error${errors.length !== 1 ? "s" : ""}`) : "",
354
+ warnings.length > 0 ? chalk.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}`) : "",
355
+ notes.length > 0 ? chalk.blue(`${notes.length} note${notes.length !== 1 ? "s" : ""}`) : ""
356
+ ].filter(Boolean).join(chalk.dim(", "));
357
+ lines.push(chalk.bold.underline(` Findings`) + chalk.dim(` (${summary})`));
298
358
  for (const f of artifact.findings) {
299
359
  const icon = f.level === "error" ? chalk.red("\u2716") : f.level === "warning" ? chalk.yellow("\u26A0") : chalk.blue("\u2139");
300
360
  lines.push(` ${icon} ${f.message}`);
@@ -302,15 +362,30 @@ function formatText(artifact) {
302
362
  }
303
363
  lines.push("");
304
364
  }
305
- if (artifact.delta !== void 0) {
306
- const deltaStr = artifact.delta > 0 ? chalk.green(`+${artifact.delta}`) : artifact.delta < 0 ? chalk.red(`${artifact.delta}`) : chalk.dim("0");
307
- lines.push(chalk.bold(" Drift Delta: ") + deltaStr + " (vs baseline)");
365
+ const actions = generatePriorityActions(artifact);
366
+ if (actions.length > 0) {
367
+ lines.push(chalk.bold.cyan("\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557"));
368
+ lines.push(chalk.bold.cyan("\u2551 Top Priority Actions \u2551"));
369
+ lines.push(chalk.bold.cyan("\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D"));
308
370
  lines.push("");
371
+ for (let i = 0; i < actions.length; i++) {
372
+ const a = actions[i];
373
+ const num = chalk.bold.cyan(` ${i + 1}.`);
374
+ lines.push(`${num} ${chalk.bold(a.title)}`);
375
+ lines.push(chalk.dim(` ${a.explanation}`));
376
+ if (a.impact) lines.push(` Impact: ${chalk.green(a.impact)}`);
377
+ lines.push("");
378
+ }
309
379
  }
310
- if (artifact.extended) {
311
- lines.push(...formatExtended(artifact.extended));
380
+ const scannedParts = [`Scanned at ${artifact.timestamp}`];
381
+ if (artifact.durationMs !== void 0) {
382
+ const secs = (artifact.durationMs / 1e3).toFixed(1);
383
+ scannedParts.push(`${secs}s`);
312
384
  }
313
- lines.push(chalk.dim(` Scanned at ${artifact.timestamp}`));
385
+ if (artifact.filesScanned !== void 0) {
386
+ scannedParts.push(`${artifact.filesScanned} file${artifact.filesScanned !== 1 ? "s" : ""} scanned`);
387
+ }
388
+ lines.push(chalk.dim(` ${scannedParts.join(" \xB7 ")}`));
314
389
  lines.push("");
315
390
  return lines.join("\n");
316
391
  }
@@ -331,7 +406,7 @@ function scoreBar(score) {
331
406
  const filled = Math.round(score / 100 * width);
332
407
  const empty = width - filled;
333
408
  const color = score >= 70 ? chalk.green : score >= 40 ? chalk.yellow : chalk.red;
334
- return color("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty)) + ` ${score}`;
409
+ return color("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty)) + ` ${Math.round(score)}`;
335
410
  }
336
411
  var CATEGORY_LABELS = {
337
412
  frontend: "Frontend",
@@ -471,6 +546,149 @@ function formatExtended(ext) {
471
546
  }
472
547
  return lines;
473
548
  }
549
+ function generatePriorityActions(artifact) {
550
+ const actions = [];
551
+ const eolProjects = artifact.projects.filter(
552
+ (p) => p.runtimeMajorsBehind !== void 0 && p.runtimeMajorsBehind >= 3
553
+ );
554
+ if (eolProjects.length > 0) {
555
+ const names = eolProjects.map((p) => p.name).join(", ");
556
+ const runtimes = eolProjects.map((p) => `${p.runtime} \u2192 ${p.runtimeLatest}`).join(", ");
557
+ actions.push({
558
+ title: `Upgrade EOL runtime${eolProjects.length > 1 ? "s" : ""} in ${names}`,
559
+ explanation: `${runtimes}. End-of-life runtimes no longer receive security patches and block ecosystem upgrades.`,
560
+ impact: `+${Math.min(eolProjects.length * 10, 30)} points (runtime & EOL scores)`,
561
+ severity: 100
562
+ });
563
+ }
564
+ const severeFrameworks = [];
565
+ for (const p of artifact.projects) {
566
+ for (const fw of p.frameworks) {
567
+ if (fw.majorsBehind !== null && fw.majorsBehind >= 3) {
568
+ severeFrameworks.push({ name: fw.name, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}`, behind: fw.majorsBehind, project: p.name });
569
+ }
570
+ }
571
+ }
572
+ if (severeFrameworks.length > 0) {
573
+ const worst = severeFrameworks.sort((a, b) => b.behind - a.behind)[0];
574
+ const others = severeFrameworks.length > 1 ? ` (+${severeFrameworks.length - 1} more)` : "";
575
+ actions.push({
576
+ title: `Upgrade ${worst.name} ${worst.fw} in ${worst.project}${others}`,
577
+ explanation: `${worst.behind} major versions behind. Major framework drift increases breaking change risk and blocks access to security fixes and performance improvements.`,
578
+ impact: `+5\u201315 points (framework score)`,
579
+ severity: 90
580
+ });
581
+ }
582
+ for (const p of artifact.projects) {
583
+ const b = p.dependencyAgeBuckets;
584
+ const total = b.current + b.oneBehind + b.twoPlusBehind;
585
+ if (total === 0) continue;
586
+ const twoPlusPct = Math.round(b.twoPlusBehind / total * 100);
587
+ if (twoPlusPct >= 40) {
588
+ actions.push({
589
+ title: `Reduce dependency rot in ${p.name} (${twoPlusPct}% severely outdated)`,
590
+ explanation: `${b.twoPlusBehind} of ${total} dependencies are 2+ majors behind. Run \`npm outdated\` and prioritise packages with known CVEs or breaking API changes.`,
591
+ impact: `+5\u201310 points (dependency score)`,
592
+ severity: 80 + twoPlusPct / 10
593
+ });
594
+ }
595
+ }
596
+ const twoMajorFrameworks = [];
597
+ for (const p of artifact.projects) {
598
+ for (const fw of p.frameworks) {
599
+ if (fw.majorsBehind === 2) {
600
+ twoMajorFrameworks.push({ name: fw.name, project: p.name, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}` });
601
+ }
602
+ }
603
+ }
604
+ const uniqueTwo = [...new Map(twoMajorFrameworks.map((f) => [f.name, f])).values()];
605
+ if (uniqueTwo.length > 0) {
606
+ const list = uniqueTwo.slice(0, 3).map((f) => `${f.name} (${f.fw})`).join(", ");
607
+ const moreCount = uniqueTwo.length > 3 ? ` +${uniqueTwo.length - 3} more` : "";
608
+ actions.push({
609
+ title: `Plan major framework upgrades: ${list}${moreCount}`,
610
+ explanation: `These frameworks are 2 major versions behind. Create upgrade tickets and check migration guides \u2014 the gap will widen with each new release.`,
611
+ impact: `+5\u201310 points (framework score)`,
612
+ severity: 60
613
+ });
614
+ }
615
+ if (artifact.extended?.breakingChangeExposure) {
616
+ const bc = artifact.extended.breakingChangeExposure;
617
+ const total = bc.deprecatedPackages.length + bc.legacyPolyfills.length;
618
+ if (total > 0) {
619
+ const items = [...bc.deprecatedPackages, ...bc.legacyPolyfills].slice(0, 5).join(", ");
620
+ const moreCount = total > 5 ? ` +${total - 5} more` : "";
621
+ actions.push({
622
+ title: `Replace deprecated/legacy packages: ${items}${moreCount}`,
623
+ explanation: `${total} package${total !== 1 ? "s" : ""} are deprecated or legacy polyfills. These receive no updates and may have known vulnerabilities.`,
624
+ severity: 55
625
+ });
626
+ }
627
+ }
628
+ if (artifact.extended?.dependencyGraph) {
629
+ const dg = artifact.extended.dependencyGraph;
630
+ const phantomCount = dg.phantomDependencies.length;
631
+ if (phantomCount >= 10) {
632
+ let detail = `Packages used in code but not declared in package.json. These rely on transitive installs and can break unpredictably when other packages update.`;
633
+ const details = dg.phantomDependencyDetails;
634
+ if (details && details.length > 0) {
635
+ const byPath = /* @__PURE__ */ new Map();
636
+ for (const d of details) {
637
+ if (!byPath.has(d.sourcePath)) byPath.set(d.sourcePath, []);
638
+ byPath.get(d.sourcePath).push({ package: d.package, spec: d.spec });
639
+ }
640
+ const pathLines = [];
641
+ let shown = 0;
642
+ for (const [srcPath, pkgs] of byPath) {
643
+ if (shown >= 10) break;
644
+ pathLines.push(`
645
+ ./${srcPath}`);
646
+ for (const pkg2 of pkgs) {
647
+ if (shown >= 10) break;
648
+ pathLines.push(` ${pkg2.package}: ${pkg2.spec}`);
649
+ shown++;
650
+ }
651
+ }
652
+ const remaining = phantomCount - shown;
653
+ detail += pathLines.join("");
654
+ if (remaining > 0) detail += `
655
+ ... and ${remaining} more`;
656
+ }
657
+ actions.push({
658
+ title: `Fix ${phantomCount} phantom dependencies`,
659
+ explanation: detail,
660
+ severity: 45
661
+ });
662
+ }
663
+ }
664
+ if (artifact.extended?.securityPosture) {
665
+ const sec = artifact.extended.securityPosture;
666
+ if (sec.envFilesTracked || !sec.lockfilePresent) {
667
+ const issues = [];
668
+ if (sec.envFilesTracked) issues.push(".env files are tracked in git");
669
+ if (!sec.lockfilePresent) issues.push("no lockfile found");
670
+ actions.push({
671
+ title: `Fix security posture: ${issues.join(", ")}`,
672
+ explanation: sec.envFilesTracked ? "Environment files may contain secrets. Add them to .gitignore and rotate any exposed credentials immediately." : "Without a lockfile, installs are non-deterministic. Run the install command to generate one and commit it.",
673
+ severity: 95
674
+ });
675
+ }
676
+ }
677
+ if (artifact.extended?.dependencyGraph) {
678
+ const dupes = artifact.extended.dependencyGraph.duplicatedPackages;
679
+ const highImpactDupes = dupes.filter((d) => d.versions.length >= 3);
680
+ if (highImpactDupes.length >= 3) {
681
+ const names = highImpactDupes.slice(0, 4).map((d) => `${d.name} (${d.versions.length}v)`).join(", ");
682
+ actions.push({
683
+ title: `Deduplicate heavily-versioned packages`,
684
+ explanation: `${highImpactDupes.length} packages have 3+ versions installed: ${names}. Run \`npm dedupe\` to reduce bundle size and install time.`,
685
+ severity: 35
686
+ });
687
+ }
688
+ }
689
+ actions.sort((a, b) => b.severity - a.severity);
690
+ return actions.slice(0, 5);
691
+ }
474
692
 
475
693
  // src/formatters/sarif.ts
476
694
  function formatSarif(artifact) {
@@ -554,12 +772,6 @@ function toSarifResult(finding) {
554
772
  };
555
773
  }
556
774
 
557
- // src/version.ts
558
- import { createRequire } from "module";
559
- var require2 = createRequire(import.meta.url);
560
- var pkg = require2("../package.json");
561
- var VERSION = pkg.version;
562
-
563
775
  // src/commands/scan.ts
564
776
  import * as path12 from "path";
565
777
  import { Command } from "commander";
@@ -1350,7 +1562,7 @@ var ROBOT = [
1350
1562
  ];
1351
1563
  var BRAND = [
1352
1564
  chalk2.bold.white(" V I B G R A T E"),
1353
- chalk2.dim(" Drift Intelligence Engine")
1565
+ chalk2.dim(` Drift Intelligence Engine`) + chalk2.dim(` v${VERSION}`)
1354
1566
  ];
1355
1567
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1356
1568
  var ScanProgress = class {
@@ -2021,9 +2233,11 @@ async function scanDependencyGraph(rootDir) {
2021
2233
  const lockedNames = new Set(versionMap.keys());
2022
2234
  const pkgFiles = await findPackageJsonFiles(rootDir);
2023
2235
  const phantoms = /* @__PURE__ */ new Set();
2236
+ const phantomDetails = [];
2024
2237
  for (const pjPath of pkgFiles) {
2025
2238
  try {
2026
2239
  const pj = await readJsonFile(pjPath);
2240
+ const relPath = path7.relative(rootDir, pjPath);
2027
2241
  for (const section of ["dependencies", "devDependencies"]) {
2028
2242
  const deps = pj[section];
2029
2243
  if (!deps) continue;
@@ -2031,6 +2245,7 @@ async function scanDependencyGraph(rootDir) {
2031
2245
  const ver = typeof version === "string" ? version : "";
2032
2246
  if (!lockedNames.has(name) && !ver.startsWith("workspace:")) {
2033
2247
  phantoms.add(name);
2248
+ phantomDetails.push({ package: name, spec: ver, sourcePath: relPath });
2034
2249
  }
2035
2250
  }
2036
2251
  }
@@ -2038,6 +2253,7 @@ async function scanDependencyGraph(rootDir) {
2038
2253
  }
2039
2254
  }
2040
2255
  result.phantomDependencies = [...phantoms].sort();
2256
+ result.phantomDependencyDetails = phantomDetails.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath) || a.package.localeCompare(b.package));
2041
2257
  return result;
2042
2258
  }
2043
2259
 
@@ -3447,10 +3663,12 @@ function scanServiceDependencies(projects) {
3447
3663
 
3448
3664
  // src/commands/scan.ts
3449
3665
  async function runScan(rootDir, opts) {
3666
+ const scanStart = Date.now();
3450
3667
  const config = await loadConfig(rootDir);
3451
3668
  const sem = new Semaphore(opts.concurrency);
3452
3669
  const npmCache = new NpmCache(rootDir, sem);
3453
3670
  const scanners = config.scanners;
3671
+ let filesScanned = 0;
3454
3672
  const progress = new ScanProgress(rootDir);
3455
3673
  const steps = [
3456
3674
  { id: "config", label: "Loading configuration" },
@@ -3484,6 +3702,7 @@ async function runScan(rootDir, opts) {
3484
3702
  progress.addDependencies(p.dependencies.length);
3485
3703
  progress.addFrameworks(p.frameworks.length);
3486
3704
  }
3705
+ filesScanned += nodeProjects.length;
3487
3706
  progress.addProjects(nodeProjects.length);
3488
3707
  progress.completeStep("node", `${nodeProjects.length} project${nodeProjects.length !== 1 ? "s" : ""}`, nodeProjects.length);
3489
3708
  progress.startStep("dotnet");
@@ -3492,91 +3711,130 @@ async function runScan(rootDir, opts) {
3492
3711
  progress.addDependencies(p.dependencies.length);
3493
3712
  progress.addFrameworks(p.frameworks.length);
3494
3713
  }
3714
+ filesScanned += dotnetProjects.length;
3495
3715
  progress.addProjects(dotnetProjects.length);
3496
3716
  progress.completeStep("dotnet", `${dotnetProjects.length} project${dotnetProjects.length !== 1 ? "s" : ""}`, dotnetProjects.length);
3497
3717
  const allProjects = [...nodeProjects, ...dotnetProjects];
3498
3718
  const extended = {};
3499
3719
  if (scanners !== false) {
3720
+ const scannerTasks = [];
3500
3721
  if (scanners?.platformMatrix?.enabled !== false) {
3501
3722
  progress.startStep("platform");
3502
- extended.platformMatrix = await scanPlatformMatrix(rootDir);
3503
- const nativeCount = extended.platformMatrix.nativeModules.length;
3504
- const dockerCount = extended.platformMatrix.dockerBaseImages.length;
3505
- const parts = [];
3506
- if (nativeCount > 0) parts.push(`${nativeCount} native`);
3507
- if (dockerCount > 0) parts.push(`${dockerCount} docker`);
3508
- progress.completeStep("platform", parts.join(", ") || "clean", nativeCount + dockerCount);
3723
+ scannerTasks.push(
3724
+ scanPlatformMatrix(rootDir).then((result) => {
3725
+ extended.platformMatrix = result;
3726
+ const nativeCount = result.nativeModules.length;
3727
+ const dockerCount = result.dockerBaseImages.length;
3728
+ const parts = [];
3729
+ if (nativeCount > 0) parts.push(`${nativeCount} native`);
3730
+ if (dockerCount > 0) parts.push(`${dockerCount} docker`);
3731
+ progress.completeStep("platform", parts.join(", ") || "clean", nativeCount + dockerCount);
3732
+ })
3733
+ );
3509
3734
  }
3510
3735
  if (scanners?.toolingInventory?.enabled !== false) {
3511
3736
  progress.startStep("tooling");
3512
- extended.toolingInventory = scanToolingInventory(allProjects);
3513
- const toolCount = Object.values(extended.toolingInventory).reduce((sum, arr) => sum + arr.length, 0);
3514
- progress.completeStep("tooling", `${toolCount} tool${toolCount !== 1 ? "s" : ""} mapped`, toolCount);
3737
+ scannerTasks.push(
3738
+ Promise.resolve().then(() => {
3739
+ extended.toolingInventory = scanToolingInventory(allProjects);
3740
+ const toolCount = Object.values(extended.toolingInventory).reduce((sum, arr) => sum + arr.length, 0);
3741
+ progress.completeStep("tooling", `${toolCount} tool${toolCount !== 1 ? "s" : ""} mapped`, toolCount);
3742
+ })
3743
+ );
3515
3744
  }
3516
3745
  if (scanners?.serviceDependencies?.enabled !== false) {
3517
3746
  progress.startStep("services");
3518
- extended.serviceDependencies = scanServiceDependencies(allProjects);
3519
- const svcCount = Object.values(extended.serviceDependencies).reduce((sum, arr) => sum + arr.length, 0);
3520
- progress.completeStep("services", `${svcCount} service${svcCount !== 1 ? "s" : ""} detected`, svcCount);
3747
+ scannerTasks.push(
3748
+ Promise.resolve().then(() => {
3749
+ extended.serviceDependencies = scanServiceDependencies(allProjects);
3750
+ const svcCount = Object.values(extended.serviceDependencies).reduce((sum, arr) => sum + arr.length, 0);
3751
+ progress.completeStep("services", `${svcCount} service${svcCount !== 1 ? "s" : ""} detected`, svcCount);
3752
+ })
3753
+ );
3521
3754
  }
3522
3755
  if (scanners?.breakingChangeExposure?.enabled !== false) {
3523
3756
  progress.startStep("breaking");
3524
- extended.breakingChangeExposure = scanBreakingChangeExposure(allProjects);
3525
- const bc = extended.breakingChangeExposure;
3526
- const bcTotal = bc.deprecatedPackages.length + bc.legacyPolyfills.length;
3527
- progress.completeStep(
3528
- "breaking",
3529
- bcTotal > 0 ? `${bc.deprecatedPackages.length} deprecated, ${bc.legacyPolyfills.length} polyfills` : "none found",
3530
- bcTotal
3757
+ scannerTasks.push(
3758
+ Promise.resolve().then(() => {
3759
+ extended.breakingChangeExposure = scanBreakingChangeExposure(allProjects);
3760
+ const bc = extended.breakingChangeExposure;
3761
+ const bcTotal = bc.deprecatedPackages.length + bc.legacyPolyfills.length;
3762
+ progress.completeStep(
3763
+ "breaking",
3764
+ bcTotal > 0 ? `${bc.deprecatedPackages.length} deprecated, ${bc.legacyPolyfills.length} polyfills` : "none found",
3765
+ bcTotal
3766
+ );
3767
+ })
3531
3768
  );
3532
3769
  }
3533
3770
  if (scanners?.securityPosture?.enabled !== false) {
3534
3771
  progress.startStep("security");
3535
- extended.securityPosture = await scanSecurityPosture(rootDir);
3536
- const sec = extended.securityPosture;
3537
- const secDetail = sec.lockfilePresent ? `lockfile \u2714${sec.gitignoreCoversEnv ? " \xB7 .env \u2714" : " \xB7 .env \u2716"}` : "no lockfile";
3538
- progress.completeStep("security", secDetail);
3772
+ scannerTasks.push(
3773
+ scanSecurityPosture(rootDir).then((result) => {
3774
+ extended.securityPosture = result;
3775
+ const secDetail = result.lockfilePresent ? `lockfile \u2714${result.gitignoreCoversEnv ? " \xB7 .env \u2714" : " \xB7 .env \u2716"}` : "no lockfile";
3776
+ progress.completeStep("security", secDetail);
3777
+ })
3778
+ );
3539
3779
  }
3540
3780
  if (scanners?.buildDeploy?.enabled !== false) {
3541
3781
  progress.startStep("build");
3542
- extended.buildDeploy = await scanBuildDeploy(rootDir);
3543
- const bd = extended.buildDeploy;
3544
- const bdParts = [];
3545
- if (bd.ci.length > 0) bdParts.push(bd.ci.join(", "));
3546
- if (bd.docker.dockerfileCount > 0) bdParts.push(`${bd.docker.dockerfileCount} Dockerfile${bd.docker.dockerfileCount !== 1 ? "s" : ""}`);
3547
- progress.completeStep("build", bdParts.join(" \xB7 ") || "none detected");
3782
+ scannerTasks.push(
3783
+ scanBuildDeploy(rootDir).then((result) => {
3784
+ extended.buildDeploy = result;
3785
+ const bdParts = [];
3786
+ if (result.ci.length > 0) bdParts.push(result.ci.join(", "));
3787
+ if (result.docker.dockerfileCount > 0) bdParts.push(`${result.docker.dockerfileCount} Dockerfile${result.docker.dockerfileCount !== 1 ? "s" : ""}`);
3788
+ progress.completeStep("build", bdParts.join(" \xB7 ") || "none detected");
3789
+ })
3790
+ );
3548
3791
  }
3549
3792
  if (scanners?.tsModernity?.enabled !== false) {
3550
3793
  progress.startStep("ts");
3551
- extended.tsModernity = await scanTsModernity(rootDir);
3552
- const ts = extended.tsModernity;
3553
- const tsParts = [];
3554
- if (ts.typescriptVersion) tsParts.push(`v${ts.typescriptVersion}`);
3555
- if (ts.strict === true) tsParts.push("strict");
3556
- if (ts.moduleType) tsParts.push(ts.moduleType.toUpperCase());
3557
- progress.completeStep("ts", tsParts.join(" \xB7 ") || "no tsconfig");
3794
+ scannerTasks.push(
3795
+ scanTsModernity(rootDir).then((result) => {
3796
+ extended.tsModernity = result;
3797
+ const tsParts = [];
3798
+ if (result.typescriptVersion) tsParts.push(`v${result.typescriptVersion}`);
3799
+ if (result.strict === true) tsParts.push("strict");
3800
+ if (result.moduleType) tsParts.push(result.moduleType.toUpperCase());
3801
+ progress.completeStep("ts", tsParts.join(" \xB7 ") || "no tsconfig");
3802
+ })
3803
+ );
3558
3804
  }
3559
3805
  if (scanners?.fileHotspots?.enabled !== false) {
3560
3806
  progress.startStep("hotspots");
3561
- extended.fileHotspots = await scanFileHotspots(rootDir);
3562
- progress.completeStep("hotspots", `${extended.fileHotspots.totalFiles} files`, extended.fileHotspots.totalFiles);
3807
+ scannerTasks.push(
3808
+ scanFileHotspots(rootDir).then((result) => {
3809
+ extended.fileHotspots = result;
3810
+ progress.completeStep("hotspots", `${result.totalFiles} files`, result.totalFiles);
3811
+ })
3812
+ );
3563
3813
  }
3564
3814
  if (scanners?.dependencyGraph?.enabled !== false) {
3565
3815
  progress.startStep("depgraph");
3566
- extended.dependencyGraph = await scanDependencyGraph(rootDir);
3567
- const dg = extended.dependencyGraph;
3568
- const dgDetail = dg.lockfileType ? `${dg.lockfileType} \xB7 ${dg.totalUnique} unique` : "no lockfile";
3569
- progress.completeStep("depgraph", dgDetail, dg.totalUnique);
3816
+ scannerTasks.push(
3817
+ scanDependencyGraph(rootDir).then((result) => {
3818
+ extended.dependencyGraph = result;
3819
+ const dgDetail = result.lockfileType ? `${result.lockfileType} \xB7 ${result.totalUnique} unique` : "no lockfile";
3820
+ progress.completeStep("depgraph", dgDetail, result.totalUnique);
3821
+ })
3822
+ );
3570
3823
  }
3571
3824
  if (scanners?.dependencyRisk?.enabled !== false) {
3572
3825
  progress.startStep("deprisk");
3573
- extended.dependencyRisk = scanDependencyRisk(allProjects);
3574
- const dr = extended.dependencyRisk;
3575
- const drParts = [];
3576
- if (dr.deprecatedPackages.length > 0) drParts.push(`${dr.deprecatedPackages.length} deprecated`);
3577
- if (dr.nativeModulePackages.length > 0) drParts.push(`${dr.nativeModulePackages.length} native`);
3578
- progress.completeStep("deprisk", drParts.join(", ") || "low risk");
3826
+ scannerTasks.push(
3827
+ Promise.resolve().then(() => {
3828
+ extended.dependencyRisk = scanDependencyRisk(allProjects);
3829
+ const dr = extended.dependencyRisk;
3830
+ const drParts = [];
3831
+ if (dr.deprecatedPackages.length > 0) drParts.push(`${dr.deprecatedPackages.length} deprecated`);
3832
+ if (dr.nativeModulePackages.length > 0) drParts.push(`${dr.nativeModulePackages.length} native`);
3833
+ progress.completeStep("deprisk", drParts.join(", ") || "low risk");
3834
+ })
3835
+ );
3579
3836
  }
3837
+ await Promise.all(scannerTasks);
3580
3838
  }
3581
3839
  progress.startStep("drift");
3582
3840
  const drift = computeDriftScore(allProjects);
@@ -3596,6 +3854,15 @@ async function runScan(rootDir, opts) {
3596
3854
  if (allProjects.length === 0) {
3597
3855
  console.log(chalk3.yellow("No projects found."));
3598
3856
  }
3857
+ if (extended.fileHotspots) filesScanned += extended.fileHotspots.totalFiles;
3858
+ if (extended.securityPosture) filesScanned += 1;
3859
+ if (extended.tsModernity?.typescriptVersion) filesScanned += 1;
3860
+ if (extended.dependencyGraph?.lockfileType) filesScanned += 1;
3861
+ if (extended.buildDeploy) {
3862
+ filesScanned += extended.buildDeploy.docker.dockerfileCount;
3863
+ filesScanned += extended.buildDeploy.ci.length;
3864
+ }
3865
+ const durationMs = Date.now() - scanStart;
3599
3866
  const artifact = {
3600
3867
  schemaVersion: "1.0",
3601
3868
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -3605,7 +3872,9 @@ async function runScan(rootDir, opts) {
3605
3872
  projects: allProjects,
3606
3873
  drift,
3607
3874
  findings,
3608
- ...Object.keys(extended).length > 0 ? { extended } : {}
3875
+ ...Object.keys(extended).length > 0 ? { extended } : {},
3876
+ durationMs,
3877
+ filesScanned
3609
3878
  };
3610
3879
  if (opts.baseline) {
3611
3880
  const baselinePath = path12.resolve(opts.baseline);
@@ -3689,9 +3958,9 @@ export {
3689
3958
  writeDefaultConfig,
3690
3959
  computeDriftScore,
3691
3960
  generateFindings,
3961
+ VERSION,
3692
3962
  formatText,
3693
3963
  formatSarif,
3694
- VERSION,
3695
3964
  runScan,
3696
3965
  scanCommand
3697
3966
  };
@@ -8,7 +8,10 @@ function formatMarkdown(artifact) {
8
8
  lines.push(`| **Drift Score** | ${artifact.drift.score}/100 |`);
9
9
  lines.push(`| **Risk Level** | ${artifact.drift.riskLevel.toUpperCase()} |`);
10
10
  lines.push(`| **Projects** | ${artifact.projects.length} |`);
11
- lines.push(`| **Scanned** | ${artifact.timestamp} |`);
11
+ const scannedMeta = [artifact.timestamp];
12
+ if (artifact.durationMs !== void 0) scannedMeta.push(`${(artifact.durationMs / 1e3).toFixed(1)}s`);
13
+ if (artifact.filesScanned !== void 0) scannedMeta.push(`${artifact.filesScanned} files`);
14
+ lines.push(`| **Scanned** | ${scannedMeta.join(" \xB7 ")} |`);
12
15
  if (artifact.vcs) {
13
16
  lines.push(`| **VCS** | ${artifact.vcs.type} |`);
14
17
  if (artifact.vcs.branch) lines.push(`| **Branch** | ${artifact.vcs.branch} |`);
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  formatMarkdown
4
- } from "./chunk-AMOJCCF5.js";
4
+ } from "./chunk-VXZT34Y5.js";
5
5
  import {
6
6
  baselineCommand
7
- } from "./chunk-3X3ZMVHI.js";
7
+ } from "./chunk-NTRKEIKP.js";
8
8
  import {
9
9
  VERSION,
10
10
  ensureDir,
@@ -15,7 +15,7 @@ import {
15
15
  scanCommand,
16
16
  writeDefaultConfig,
17
17
  writeTextFile
18
- } from "./chunk-VXEZ7APL.js";
18
+ } from "./chunk-VMNBKARQ.js";
19
19
 
20
20
  // src/cli.ts
21
21
  import { Command as Command6 } from "commander";
@@ -38,7 +38,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
38
38
  console.log(chalk.green("\u2714") + ` Created ${chalk.bold("vibgrate.config.ts")}`);
39
39
  }
40
40
  if (opts.baseline) {
41
- const { runBaseline } = await import("./baseline-D5UDXOEJ.js");
41
+ const { runBaseline } = await import("./baseline-35XRSRAD.js");
42
42
  await runBaseline(rootDir);
43
43
  }
44
44
  console.log("");
package/dist/index.d.ts CHANGED
@@ -43,6 +43,8 @@ interface DriftScore {
43
43
  dependencyScore: number;
44
44
  eolScore: number;
45
45
  };
46
+ /** Which components had sufficient data to score. Missing = no data available. */
47
+ measured?: ('runtime' | 'framework' | 'dependency' | 'eol')[];
46
48
  }
47
49
  interface Finding {
48
50
  ruleId: string;
@@ -70,6 +72,10 @@ interface ScanArtifact {
70
72
  baseline?: string;
71
73
  delta?: number;
72
74
  extended?: ExtendedScanResults;
75
+ /** Scan wall-clock duration in milliseconds */
76
+ durationMs?: number;
77
+ /** Number of manifest/config files scanned */
78
+ filesScanned?: number;
73
79
  }
74
80
  interface ScanOptions {
75
81
  out?: string;
@@ -130,12 +136,18 @@ interface DuplicatedPackage {
130
136
  versions: string[];
131
137
  consumers: number;
132
138
  }
139
+ interface PhantomDependency {
140
+ package: string;
141
+ spec: string;
142
+ sourcePath: string;
143
+ }
133
144
  interface DependencyGraphResult {
134
145
  lockfileType: string | null;
135
146
  totalUnique: number;
136
147
  totalInstalled: number;
137
148
  duplicatedPackages: DuplicatedPackage[];
138
149
  phantomDependencies: string[];
150
+ phantomDependencyDetails?: PhantomDependency[];
139
151
  }
140
152
  interface InventoryItem {
141
153
  name: string;
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  formatMarkdown
3
- } from "./chunk-AMOJCCF5.js";
3
+ } from "./chunk-VXZT34Y5.js";
4
4
  import {
5
5
  computeDriftScore,
6
6
  formatSarif,
7
7
  formatText,
8
8
  generateFindings,
9
9
  runScan
10
- } from "./chunk-VXEZ7APL.js";
10
+ } from "./chunk-VMNBKARQ.js";
11
11
  export {
12
12
  computeDriftScore,
13
13
  formatMarkdown,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibgrate/cli",
3
- "version": "0.1.3",
3
+ "version": "1.0.1",
4
4
  "description": "CLI for measuring upgrade drift across Node & .NET projects",
5
5
  "type": "module",
6
6
  "bin": {