@vibgrate/cli 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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-BTIIFIOD.js";
5
+ import "./chunk-WO6EZ6AF.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-WO6EZ6AF.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) {
@@ -263,11 +295,12 @@ function formatText(artifact) {
263
295
  lines.push(chalk.bold(" VCS: ") + vcsParts.join(" "));
264
296
  }
265
297
  lines.push("");
298
+ const m = new Set(artifact.drift.measured ?? ["runtime", "framework", "dependency", "eol"]);
266
299
  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)}`);
300
+ lines.push(` Runtime: ${m.has("runtime") ? scoreBar(artifact.drift.components.runtimeScore) : chalk.dim("n/a")}`);
301
+ lines.push(` Frameworks: ${m.has("framework") ? scoreBar(artifact.drift.components.frameworkScore) : chalk.dim("n/a")}`);
302
+ lines.push(` Dependencies: ${m.has("dependency") ? scoreBar(artifact.drift.components.dependencyScore) : chalk.dim("n/a")}`);
303
+ lines.push(` EOL Risk: ${m.has("eol") ? scoreBar(artifact.drift.components.eolScore) : chalk.dim("n/a")}`);
271
304
  lines.push("");
272
305
  for (const project of artifact.projects) {
273
306
  lines.push(chalk.bold(` \u2500\u2500 ${project.name} `) + chalk.dim(`(${project.type}) ${project.path}`));
@@ -293,8 +326,24 @@ function formatText(artifact) {
293
326
  }
294
327
  lines.push("");
295
328
  }
329
+ if (artifact.delta !== void 0) {
330
+ const deltaStr = artifact.delta > 0 ? chalk.green(`+${artifact.delta}`) : artifact.delta < 0 ? chalk.red(`${artifact.delta}`) : chalk.dim("0");
331
+ lines.push(chalk.bold(" Drift Delta: ") + deltaStr + " (vs baseline)");
332
+ lines.push("");
333
+ }
334
+ if (artifact.extended) {
335
+ lines.push(...formatExtended(artifact.extended));
336
+ }
296
337
  if (artifact.findings.length > 0) {
297
- lines.push(chalk.bold.underline(" Findings"));
338
+ const errors = artifact.findings.filter((f) => f.level === "error");
339
+ const warnings = artifact.findings.filter((f) => f.level === "warning");
340
+ const notes = artifact.findings.filter((f) => f.level === "note");
341
+ const summary = [
342
+ errors.length > 0 ? chalk.red(`${errors.length} error${errors.length !== 1 ? "s" : ""}`) : "",
343
+ warnings.length > 0 ? chalk.yellow(`${warnings.length} warning${warnings.length !== 1 ? "s" : ""}`) : "",
344
+ notes.length > 0 ? chalk.blue(`${notes.length} note${notes.length !== 1 ? "s" : ""}`) : ""
345
+ ].filter(Boolean).join(chalk.dim(", "));
346
+ lines.push(chalk.bold.underline(` Findings`) + chalk.dim(` (${summary})`));
298
347
  for (const f of artifact.findings) {
299
348
  const icon = f.level === "error" ? chalk.red("\u2716") : f.level === "warning" ? chalk.yellow("\u26A0") : chalk.blue("\u2139");
300
349
  lines.push(` ${icon} ${f.message}`);
@@ -302,13 +351,20 @@ function formatText(artifact) {
302
351
  }
303
352
  lines.push("");
304
353
  }
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)");
354
+ const actions = generatePriorityActions(artifact);
355
+ if (actions.length > 0) {
356
+ 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"));
357
+ lines.push(chalk.bold.cyan("\u2551 Top Priority Actions \u2551"));
358
+ 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
359
  lines.push("");
309
- }
310
- if (artifact.extended) {
311
- lines.push(...formatExtended(artifact.extended));
360
+ for (let i = 0; i < actions.length; i++) {
361
+ const a = actions[i];
362
+ const num = chalk.bold.cyan(` ${i + 1}.`);
363
+ lines.push(`${num} ${chalk.bold(a.title)}`);
364
+ lines.push(chalk.dim(` ${a.explanation}`));
365
+ if (a.impact) lines.push(` Impact: ${chalk.green(a.impact)}`);
366
+ lines.push("");
367
+ }
312
368
  }
313
369
  lines.push(chalk.dim(` Scanned at ${artifact.timestamp}`));
314
370
  lines.push("");
@@ -331,7 +387,7 @@ function scoreBar(score) {
331
387
  const filled = Math.round(score / 100 * width);
332
388
  const empty = width - filled;
333
389
  const color = score >= 70 ? chalk.green : score >= 40 ? chalk.yellow : chalk.red;
334
- return color("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty)) + ` ${score}`;
390
+ return color("\u2588".repeat(filled)) + chalk.dim("\u2591".repeat(empty)) + ` ${Math.round(score)}`;
335
391
  }
336
392
  var CATEGORY_LABELS = {
337
393
  frontend: "Frontend",
@@ -471,6 +527,149 @@ function formatExtended(ext) {
471
527
  }
472
528
  return lines;
473
529
  }
530
+ function generatePriorityActions(artifact) {
531
+ const actions = [];
532
+ const eolProjects = artifact.projects.filter(
533
+ (p) => p.runtimeMajorsBehind !== void 0 && p.runtimeMajorsBehind >= 3
534
+ );
535
+ if (eolProjects.length > 0) {
536
+ const names = eolProjects.map((p) => p.name).join(", ");
537
+ const runtimes = eolProjects.map((p) => `${p.runtime} \u2192 ${p.runtimeLatest}`).join(", ");
538
+ actions.push({
539
+ title: `Upgrade EOL runtime${eolProjects.length > 1 ? "s" : ""} in ${names}`,
540
+ explanation: `${runtimes}. End-of-life runtimes no longer receive security patches and block ecosystem upgrades.`,
541
+ impact: `+${Math.min(eolProjects.length * 10, 30)} points (runtime & EOL scores)`,
542
+ severity: 100
543
+ });
544
+ }
545
+ const severeFrameworks = [];
546
+ for (const p of artifact.projects) {
547
+ for (const fw of p.frameworks) {
548
+ if (fw.majorsBehind !== null && fw.majorsBehind >= 3) {
549
+ severeFrameworks.push({ name: fw.name, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}`, behind: fw.majorsBehind, project: p.name });
550
+ }
551
+ }
552
+ }
553
+ if (severeFrameworks.length > 0) {
554
+ const worst = severeFrameworks.sort((a, b) => b.behind - a.behind)[0];
555
+ const others = severeFrameworks.length > 1 ? ` (+${severeFrameworks.length - 1} more)` : "";
556
+ actions.push({
557
+ title: `Upgrade ${worst.name} ${worst.fw} in ${worst.project}${others}`,
558
+ explanation: `${worst.behind} major versions behind. Major framework drift increases breaking change risk and blocks access to security fixes and performance improvements.`,
559
+ impact: `+5\u201315 points (framework score)`,
560
+ severity: 90
561
+ });
562
+ }
563
+ for (const p of artifact.projects) {
564
+ const b = p.dependencyAgeBuckets;
565
+ const total = b.current + b.oneBehind + b.twoPlusBehind;
566
+ if (total === 0) continue;
567
+ const twoPlusPct = Math.round(b.twoPlusBehind / total * 100);
568
+ if (twoPlusPct >= 40) {
569
+ actions.push({
570
+ title: `Reduce dependency rot in ${p.name} (${twoPlusPct}% severely outdated)`,
571
+ explanation: `${b.twoPlusBehind} of ${total} dependencies are 2+ majors behind. Run \`npm outdated\` and prioritise packages with known CVEs or breaking API changes.`,
572
+ impact: `+5\u201310 points (dependency score)`,
573
+ severity: 80 + twoPlusPct / 10
574
+ });
575
+ }
576
+ }
577
+ const twoMajorFrameworks = [];
578
+ for (const p of artifact.projects) {
579
+ for (const fw of p.frameworks) {
580
+ if (fw.majorsBehind === 2) {
581
+ twoMajorFrameworks.push({ name: fw.name, project: p.name, fw: `${fw.currentVersion} \u2192 ${fw.latestVersion}` });
582
+ }
583
+ }
584
+ }
585
+ const uniqueTwo = [...new Map(twoMajorFrameworks.map((f) => [f.name, f])).values()];
586
+ if (uniqueTwo.length > 0) {
587
+ const list = uniqueTwo.slice(0, 3).map((f) => `${f.name} (${f.fw})`).join(", ");
588
+ const moreCount = uniqueTwo.length > 3 ? ` +${uniqueTwo.length - 3} more` : "";
589
+ actions.push({
590
+ title: `Plan major framework upgrades: ${list}${moreCount}`,
591
+ explanation: `These frameworks are 2 major versions behind. Create upgrade tickets and check migration guides \u2014 the gap will widen with each new release.`,
592
+ impact: `+5\u201310 points (framework score)`,
593
+ severity: 60
594
+ });
595
+ }
596
+ if (artifact.extended?.breakingChangeExposure) {
597
+ const bc = artifact.extended.breakingChangeExposure;
598
+ const total = bc.deprecatedPackages.length + bc.legacyPolyfills.length;
599
+ if (total > 0) {
600
+ const items = [...bc.deprecatedPackages, ...bc.legacyPolyfills].slice(0, 5).join(", ");
601
+ const moreCount = total > 5 ? ` +${total - 5} more` : "";
602
+ actions.push({
603
+ title: `Replace deprecated/legacy packages: ${items}${moreCount}`,
604
+ explanation: `${total} package${total !== 1 ? "s" : ""} are deprecated or legacy polyfills. These receive no updates and may have known vulnerabilities.`,
605
+ severity: 55
606
+ });
607
+ }
608
+ }
609
+ if (artifact.extended?.dependencyGraph) {
610
+ const dg = artifact.extended.dependencyGraph;
611
+ const phantomCount = dg.phantomDependencies.length;
612
+ if (phantomCount >= 10) {
613
+ 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.`;
614
+ const details = dg.phantomDependencyDetails;
615
+ if (details && details.length > 0) {
616
+ const byPath = /* @__PURE__ */ new Map();
617
+ for (const d of details) {
618
+ if (!byPath.has(d.sourcePath)) byPath.set(d.sourcePath, []);
619
+ byPath.get(d.sourcePath).push({ package: d.package, spec: d.spec });
620
+ }
621
+ const pathLines = [];
622
+ let shown = 0;
623
+ for (const [srcPath, pkgs] of byPath) {
624
+ if (shown >= 10) break;
625
+ pathLines.push(`
626
+ ./${srcPath}`);
627
+ for (const pkg2 of pkgs) {
628
+ if (shown >= 10) break;
629
+ pathLines.push(` ${pkg2.package}: ${pkg2.spec}`);
630
+ shown++;
631
+ }
632
+ }
633
+ const remaining = phantomCount - shown;
634
+ detail += pathLines.join("");
635
+ if (remaining > 0) detail += `
636
+ ... and ${remaining} more`;
637
+ }
638
+ actions.push({
639
+ title: `Fix ${phantomCount} phantom dependencies`,
640
+ explanation: detail,
641
+ severity: 45
642
+ });
643
+ }
644
+ }
645
+ if (artifact.extended?.securityPosture) {
646
+ const sec = artifact.extended.securityPosture;
647
+ if (sec.envFilesTracked || !sec.lockfilePresent) {
648
+ const issues = [];
649
+ if (sec.envFilesTracked) issues.push(".env files are tracked in git");
650
+ if (!sec.lockfilePresent) issues.push("no lockfile found");
651
+ actions.push({
652
+ title: `Fix security posture: ${issues.join(", ")}`,
653
+ 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.",
654
+ severity: 95
655
+ });
656
+ }
657
+ }
658
+ if (artifact.extended?.dependencyGraph) {
659
+ const dupes = artifact.extended.dependencyGraph.duplicatedPackages;
660
+ const highImpactDupes = dupes.filter((d) => d.versions.length >= 3);
661
+ if (highImpactDupes.length >= 3) {
662
+ const names = highImpactDupes.slice(0, 4).map((d) => `${d.name} (${d.versions.length}v)`).join(", ");
663
+ actions.push({
664
+ title: `Deduplicate heavily-versioned packages`,
665
+ explanation: `${highImpactDupes.length} packages have 3+ versions installed: ${names}. Run \`npm dedupe\` to reduce bundle size and install time.`,
666
+ severity: 35
667
+ });
668
+ }
669
+ }
670
+ actions.sort((a, b) => b.severity - a.severity);
671
+ return actions.slice(0, 5);
672
+ }
474
673
 
475
674
  // src/formatters/sarif.ts
476
675
  function formatSarif(artifact) {
@@ -2021,9 +2220,11 @@ async function scanDependencyGraph(rootDir) {
2021
2220
  const lockedNames = new Set(versionMap.keys());
2022
2221
  const pkgFiles = await findPackageJsonFiles(rootDir);
2023
2222
  const phantoms = /* @__PURE__ */ new Set();
2223
+ const phantomDetails = [];
2024
2224
  for (const pjPath of pkgFiles) {
2025
2225
  try {
2026
2226
  const pj = await readJsonFile(pjPath);
2227
+ const relPath = path7.relative(rootDir, pjPath);
2027
2228
  for (const section of ["dependencies", "devDependencies"]) {
2028
2229
  const deps = pj[section];
2029
2230
  if (!deps) continue;
@@ -2031,6 +2232,7 @@ async function scanDependencyGraph(rootDir) {
2031
2232
  const ver = typeof version === "string" ? version : "";
2032
2233
  if (!lockedNames.has(name) && !ver.startsWith("workspace:")) {
2033
2234
  phantoms.add(name);
2235
+ phantomDetails.push({ package: name, spec: ver, sourcePath: relPath });
2034
2236
  }
2035
2237
  }
2036
2238
  }
@@ -2038,6 +2240,7 @@ async function scanDependencyGraph(rootDir) {
2038
2240
  }
2039
2241
  }
2040
2242
  result.phantomDependencies = [...phantoms].sort();
2243
+ result.phantomDependencyDetails = phantomDetails.sort((a, b) => a.sourcePath.localeCompare(b.sourcePath) || a.package.localeCompare(b.package));
2041
2244
  return result;
2042
2245
  }
2043
2246
 
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-AMOJCCF5.js";
5
5
  import {
6
6
  baselineCommand
7
- } from "./chunk-3X3ZMVHI.js";
7
+ } from "./chunk-BTIIFIOD.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-WO6EZ6AF.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-45AWVXG4.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;
@@ -130,12 +132,18 @@ interface DuplicatedPackage {
130
132
  versions: string[];
131
133
  consumers: number;
132
134
  }
135
+ interface PhantomDependency {
136
+ package: string;
137
+ spec: string;
138
+ sourcePath: string;
139
+ }
133
140
  interface DependencyGraphResult {
134
141
  lockfileType: string | null;
135
142
  totalUnique: number;
136
143
  totalInstalled: number;
137
144
  duplicatedPackages: DuplicatedPackage[];
138
145
  phantomDependencies: string[];
146
+ phantomDependencyDetails?: PhantomDependency[];
139
147
  }
140
148
  interface InventoryItem {
141
149
  name: string;
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  formatText,
8
8
  generateFindings,
9
9
  runScan
10
- } from "./chunk-VXEZ7APL.js";
10
+ } from "./chunk-WO6EZ6AF.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": "0.1.4",
4
4
  "description": "CLI for measuring upgrade drift across Node & .NET projects",
5
5
  "type": "module",
6
6
  "bin": {