@vibgrate/cli 1.0.52 → 1.0.54

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-TCYJSLL2.js";
5
- import "./chunk-TYAGUEXG.js";
4
+ } from "./chunk-CQMW6PVB.js";
5
+ import "./chunk-PQEUNAVK.js";
6
6
  import "./chunk-RNVZIZNL.js";
7
7
  export {
8
8
  baselineCommand,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runScan
3
- } from "./chunk-TYAGUEXG.js";
3
+ } from "./chunk-PQEUNAVK.js";
4
4
  import {
5
5
  writeJsonFile
6
6
  } from "./chunk-RNVZIZNL.js";
@@ -469,6 +469,17 @@ function formatExtended(ext) {
469
469
  if (bc.peerConflictsDetected) {
470
470
  lines.push(` ${chalk.red("\u26A0")} Peer dependency conflicts detected`);
471
471
  }
472
+ lines.push(` Recommendation: ${chalk.bold(bc.overallRecommendation)}`);
473
+ const projectsWithPlans = bc.projectIntelligence.filter((p) => p.packages.length > 0).slice(0, 3);
474
+ if (projectsWithPlans.length > 0) {
475
+ lines.push(" Major Upgrade Intelligence:");
476
+ for (const p of projectsWithPlans) {
477
+ lines.push(` - ${p.project} (${p.recommendation})`);
478
+ for (const pkg2 of p.packages.slice(0, 2)) {
479
+ lines.push(` \xB7 ${pkg2.package} ${pkg2.currentVersion ?? "?"} \u2192 ${pkg2.targetVersion ?? "?"} | touched ~${pkg2.usage.touchedPercent}% | ${pkg2.automatable}`);
480
+ }
481
+ }
482
+ }
472
483
  lines.push("");
473
484
  }
474
485
  }
@@ -1197,7 +1208,7 @@ var pushCommand = new Command2("push").description("Push scan results to Vibgrat
1197
1208
  });
1198
1209
 
1199
1210
  // src/commands/scan.ts
1200
- import * as path22 from "path";
1211
+ import * as path23 from "path";
1201
1212
  import { Command as Command3 } from "commander";
1202
1213
  import chalk6 from "chalk";
1203
1214
 
@@ -4945,307 +4956,253 @@ async function scanTsModernity(rootDir, cache) {
4945
4956
  }
4946
4957
 
4947
4958
  // src/scanners/breaking-change.ts
4959
+ import * as path16 from "path";
4960
+ import * as semver9 from "semver";
4948
4961
  var DEPRECATED_PACKAGES2 = /* @__PURE__ */ new Set([
4949
- // Fully deprecated / archived
4950
4962
  "request",
4951
- // deprecated 2020, use undici/node fetch
4952
4963
  "request-promise",
4953
- // deprecated with request
4954
4964
  "request-promise-native",
4955
- // deprecated with request
4956
4965
  "moment",
4957
- // deprecated, use date-fns / luxon / Temporal
4958
4966
  "node-sass",
4959
- // deprecated, use sass (Dart Sass)
4960
4967
  "tslint",
4961
- // archived, use eslint + typescript-eslint
4962
4968
  "aws-sdk",
4963
- // AWS SDK v2 EOL, use @aws-sdk/* v3
4964
4969
  "babel-core",
4965
- // replaced by @babel/core
4966
4970
  "babel-preset-env",
4967
- // replaced by @babel/preset-env
4968
4971
  "babel-preset-react",
4969
- // replaced by @babel/preset-react
4970
4972
  "babel-loader",
4971
- // 7.x deprecated, must pair with @babel/core
4972
4973
  "core-js",
4973
- // v2 deprecated (v3 ok but signals old config)
4974
4974
  "istanbul",
4975
- // replaced by nyc / c8
4976
4975
  "istanbul-instrumenter-loader",
4977
- // deprecated with istanbul
4978
4976
  "left-pad",
4979
- // meme package, use String.padStart
4980
4977
  "popper.js",
4981
- // deprecated, use @popperjs/core
4982
4978
  "create-react-class",
4983
- // deprecated React pattern
4984
4979
  "react-addons-css-transition-group",
4985
- // deprecated React addon
4986
4980
  "react-addons-test-utils",
4987
- // use react-dom/test-utils
4988
4981
  "@types/express-serve-static-core",
4989
- // now shipped with express
4990
4982
  "enzyme",
4991
- // unmaintained, use Testing Library or Vitest
4992
4983
  "enzyme-adapter-react-16",
4993
- // unmaintained
4994
4984
  "enzyme-adapter-react-17",
4995
- // unmaintained
4996
4985
  "react-hot-loader",
4997
- // deprecated, use React Fast Refresh
4998
4986
  "react-loadable",
4999
- // unmaintained, use React.lazy
5000
4987
  "react-router-dom-v5-compat",
5001
- // migration shim
5002
4988
  "redux-thunk",
5003
- // bundled into @reduxjs/toolkit since v1.5
5004
4989
  "redux-saga",
5005
- // declining, consider RTK Query
5006
4990
  "recompose",
5007
- // archived, use hooks
5008
4991
  "classnames",
5009
- // works but clsx is faster & smaller
5010
4992
  "glamor",
5011
- // deprecated CSS-in-JS
5012
4993
  "radium",
5013
- // deprecated CSS-in-JS
5014
4994
  "material-ui",
5015
- // replaced by @mui/material
5016
4995
  "@material-ui/core",
5017
- // replaced by @mui/material
5018
- // Unmaintained / abandoned
5019
4996
  "bower",
5020
- // dead package manager
5021
4997
  "grunt",
5022
- // superseded by npm scripts / modern bundlers
5023
4998
  "gulp",
5024
- // declining, modern bundlers preferred
5025
4999
  "browserify",
5026
- // superseded by modern bundlers
5027
5000
  "coffee-script",
5028
- // CoffeeScript 1.x deprecated
5029
5001
  "coffeescript",
5030
- // declining
5031
5002
  "jade",
5032
- // renamed to pug
5033
5003
  "nomnom",
5034
- // deprecated CLI parser
5035
5004
  "optimist",
5036
- // deprecated CLI parser, use yargs/commander
5037
5005
  "minimist",
5038
- // unmaintained, use mri or parseArgs
5039
5006
  "colors",
5040
- // supply chain compromised, use chalk/picocolors
5041
5007
  "faker",
5042
- // supply chain compromised, use @faker-js/faker
5043
5008
  "event-stream",
5044
- // supply chain incident
5045
5009
  "ua-parser-js",
5046
- // had supply chain incident
5047
5010
  "caniuse-db",
5048
- // replaced by caniuse-lite
5049
5011
  "circular-json",
5050
- // deprecated, use flatted
5051
5012
  "mkdirp",
5052
- // Node has fs.mkdir recursive since v10
5053
5013
  "rimraf",
5054
- // Node has fs.rm recursive since v14
5055
5014
  "glob",
5056
- // consider fast-glob or Node fs.glob
5057
5015
  "swig",
5058
- // abandoned template engine
5059
5016
  "dustjs-linkedin",
5060
- // abandoned template engine
5061
5017
  "hogan.js",
5062
- // abandoned template engine
5063
5018
  "passport-local-mongoose",
5064
- // low maintenance
5065
- // Known breaking-change magnets
5066
5019
  "@angular/http",
5067
- // removed in Angular 15+
5068
5020
  "rxjs-compat",
5069
- // migration shim for RxJS 5→6
5070
5021
  "protractor",
5071
- // deprecated by Angular team
5072
5022
  "karma",
5073
- // deprecated, Vitest/Jest preferred
5074
5023
  "karma-jasmine",
5075
- // deprecated with Karma
5076
5024
  "jasmine"
5077
- // declining in usage
5078
5025
  ]);
5079
5026
  var LEGACY_POLYFILLS = /* @__PURE__ */ new Set([
5080
- // Built-in fetch & web APIs (Node 18+)
5081
5027
  "node-fetch",
5082
- // native fetch since Node 18
5083
5028
  "cross-fetch",
5084
- // native fetch since Node 18
5085
5029
  "isomorphic-fetch",
5086
- // native fetch since Node 18
5087
5030
  "whatwg-fetch",
5088
- // native fetch since Node 18
5089
5031
  "abort-controller",
5090
- // native AbortController since Node 15
5091
5032
  "form-data",
5092
- // native FormData since Node 18
5093
5033
  "formdata-polyfill",
5094
- // native FormData since Node 18
5095
5034
  "web-streams-polyfill",
5096
- // native ReadableStream since Node 18
5097
5035
  "whatwg-url",
5098
- // native URL since Node 10
5099
5036
  "url-parse",
5100
- // native URL since Node 10
5101
5037
  "domexception",
5102
- // native DOMException since Node 17
5103
5038
  "abortcontroller-polyfill",
5104
- // native since Node 15
5105
- // Built-in Node modules shimmed for browser
5106
5039
  "querystring",
5107
- // URLSearchParams preferred, native in Node
5108
5040
  "string_decoder",
5109
- // native TextDecoder preferred
5110
5041
  "buffer",
5111
- // native Buffer in Node, Uint8Array in browser
5112
5042
  "events",
5113
- // native EventTarget preferred
5114
5043
  "path-browserify",
5115
- // browser shim
5116
5044
  "stream-browserify",
5117
- // browser shim
5118
5045
  "stream-http",
5119
- // browser shim
5120
5046
  "https-browserify",
5121
- // browser shim
5122
5047
  "os-browserify",
5123
- // browser shim
5124
5048
  "crypto-browserify",
5125
- // native Web Crypto API
5126
5049
  "assert",
5127
- // native assert in Node, console.assert in browser
5128
5050
  "util",
5129
- // Node native, deprecations in browser bundles
5130
5051
  "process",
5131
- // shimmed, usually unnecessary
5132
5052
  "timers-browserify",
5133
- // native timers
5134
5053
  "tty-browserify",
5135
- // rarely needed
5136
5054
  "vm-browserify",
5137
- // rarely needed
5138
5055
  "domain-browser",
5139
- // domains deprecated in Node
5140
5056
  "punycode",
5141
- // native in URL since Node 10
5142
5057
  "readable-stream",
5143
- // Node streams polyfill, usually unnecessary
5144
- // ES / language polyfills for ES2015+ (Node 18+ has all)
5145
5058
  "es6-promise",
5146
- // native Promise since Node 4
5147
5059
  "promise-polyfill",
5148
- // native Promise
5149
5060
  "es6-symbol",
5150
- // native Symbol since Node 4
5151
5061
  "es6-map",
5152
- // native Map since Node 4
5153
5062
  "es6-set",
5154
- // native Set since Node 4
5155
5063
  "es6-weak-map",
5156
- // native WeakMap since Node 4
5157
5064
  "es6-iterator",
5158
- // native iterators
5159
5065
  "object-assign",
5160
- // native Object.assign since Node 4
5161
5066
  "object.assign",
5162
- // same
5163
5067
  "array.prototype.find",
5164
- // native since ES2015
5165
5068
  "array.prototype.findindex",
5166
- // native since ES2015
5167
5069
  "array.prototype.flat",
5168
- // native since Node 11
5169
5070
  "array.prototype.flatmap",
5170
- // native since Node 11
5171
5071
  "array-includes",
5172
- // native Array.includes since Node 6
5173
5072
  "string.prototype.startswith",
5174
- // native since ES2015
5175
5073
  "string.prototype.endswith",
5176
- // native since ES2015
5177
5074
  "string.prototype.includes",
5178
- // native since ES2015
5179
5075
  "string.prototype.padstart",
5180
- // native since Node 8
5181
5076
  "string.prototype.padend",
5182
- // native since Node 8
5183
5077
  "string.prototype.matchall",
5184
- // native since Node 12
5185
5078
  "string.prototype.replaceall",
5186
- // native since Node 15
5187
5079
  "string.prototype.trimstart",
5188
- // native since Node 10
5189
5080
  "string.prototype.trimend",
5190
- // native since Node 10
5191
5081
  "string.prototype.at",
5192
- // native since Node 16.6
5193
5082
  "object.entries",
5194
- // native since Node 7
5195
5083
  "object.values",
5196
- // native since Node 7
5197
5084
  "object.fromentries",
5198
- // native since Node 12
5199
5085
  "globalthis",
5200
- // native globalThis since Node 12
5201
5086
  "symbol-observable",
5202
- // TC39 withdrawn
5203
5087
  "setimmediate",
5204
- // Node-only, setTimeout(fn, 0) preferred
5205
5088
  "regenerator-runtime",
5206
- // async/await native since Node 8
5207
5089
  "@babel/polyfill",
5208
- // deprecated, use core-js directly
5209
5090
  "whatwg-encoding",
5210
- // native TextEncoder/TextDecoder
5211
5091
  "text-encoding",
5212
- // native TextEncoder/TextDecoder since Node 11
5213
5092
  "encoding",
5214
- // native TextEncoder/TextDecoder
5215
5093
  "unorm",
5216
- // native String.normalize since Node 0.12
5217
5094
  "number.isnan",
5218
- // native Number.isNaN since Node 0.12
5219
5095
  "is-nan",
5220
- // native Number.isNaN
5221
5096
  "has-symbols",
5222
- // native Symbol detection
5223
5097
  "has",
5224
- // native Object.hasOwn since Node 16.9
5225
5098
  "hasown",
5226
- // shim for Object.hasOwn
5227
5099
  "safe-buffer",
5228
- // Buffer.from available since Node 5.10
5229
5100
  "safer-buffer"
5230
- // Buffer.from available since Node 5.10
5231
5101
  ]);
5232
- function scanBreakingChangeExposure(projects) {
5102
+ var BREAKING_SIGNAL_PHRASES = [
5103
+ "BREAKING",
5104
+ "Breaking Change",
5105
+ "Removed",
5106
+ "Deprecated",
5107
+ "Migration",
5108
+ "Rename",
5109
+ "Drop support"
5110
+ ];
5111
+ var PACKAGE_PLAYBOOKS = {
5112
+ vue: {
5113
+ impactedFeatures: ["Options API-heavy components", "Deprecated hooks", "Template filters", "$listeners / $attrs merge behavior", "Mixin-heavy architecture"],
5114
+ usagePatterns: [/\bexport\s+default\s*\{/, /\bmixins\s*:/, /\bfilters\s*:/, /\$listeners\b/, /beforeDestroy\b/, /destroyed\b/],
5115
+ automation: "manual"
5116
+ },
5117
+ "react-router-dom": {
5118
+ impactedFeatures: ["Switch/Route v5 patterns", "history prop mutation flows", "withRouter HOC usage"],
5119
+ usagePatterns: [/\bSwitch\b/, /\bwithRouter\b/, /\bhistory\./, /<Route\s+component=/],
5120
+ automation: "deterministic-recipe"
5121
+ },
5122
+ eslint: {
5123
+ impactedFeatures: ["Flat config migration", ".eslintrc plugin resolution", "legacy parser/plugin options"],
5124
+ usagePatterns: [/\.eslintrc/, /eslintConfig/, /module\.exports\s*=\s*\[/, /extends\s*:/],
5125
+ automation: "deterministic-recipe"
5126
+ },
5127
+ typescript: {
5128
+ impactedFeatures: ["Stricter type checks", "tsconfig option removals", "moduleResolution defaults changes"],
5129
+ usagePatterns: [/tsconfig\.json/, /strictNullChecks/, /noImplicitAny/, /moduleResolution/],
5130
+ automation: "manual"
5131
+ },
5132
+ "@angular/core": {
5133
+ impactedFeatures: ["NgModule bootstrap assumptions", "Standalone component migration", "Deprecated lifecycle signatures"],
5134
+ usagePatterns: [/@NgModule\b/, /ngOnInit\(/, /providers\s*:/],
5135
+ automation: "codemod-available",
5136
+ codemod: "ng update"
5137
+ }
5138
+ };
5139
+ function detectDecision(majorItems, manualHotspots, codemodItems) {
5140
+ if (majorItems === 0) return "do-nothing";
5141
+ if (codemodItems > 0 && manualHotspots === 0) return "codemod-available";
5142
+ if (manualHotspots > 0) return "manual-hotspots";
5143
+ if (majorItems <= 2) return "upgrade-safely-now";
5144
+ return "plan-major-upgrade";
5145
+ }
5146
+ function normalizeMajor(version) {
5147
+ if (!version) return null;
5148
+ const parsed = semver9.coerce(version);
5149
+ return parsed?.major ?? null;
5150
+ }
5151
+ function resolveCurrentVersion(dep) {
5152
+ if (dep.resolvedVersion && semver9.valid(semver9.coerce(dep.resolvedVersion))) return semver9.coerce(dep.resolvedVersion)?.version ?? null;
5153
+ const min = semver9.minVersion(dep.currentSpec);
5154
+ return min?.version ?? null;
5155
+ }
5156
+ function listInterimMajorTargets(current, target) {
5157
+ if (target <= current + 1) return [];
5158
+ const out = [];
5159
+ for (let m = current + 1; m < target; m++) out.push(`${m}.x`);
5160
+ return out;
5161
+ }
5162
+ async function buildProjectUsageIndex(project, rootDir, fileCache, candidatePackages) {
5163
+ const index = /* @__PURE__ */ new Map();
5164
+ for (const pkg2 of candidatePackages) index.set(pkg2, { importSites: 0, filesTouched: 0, patternHits: 0 });
5165
+ const entries = await fileCache.walkDir(rootDir);
5166
+ const projectPrefix = project.path.replace(/\\/g, "/").replace(/^\.\//, "");
5167
+ const projectEntries = entries.filter((e) => e.isFile && e.relPath.replace(/\\/g, "/").startsWith(projectPrefix));
5168
+ const codeEntries = projectEntries.filter((e) => /\.(ts|tsx|js|jsx|vue|mjs|cjs|json)$/.test(e.name));
5169
+ for (const file of codeEntries) {
5170
+ let content = "";
5171
+ try {
5172
+ content = await fileCache.readTextFile(path16.join(rootDir, file.relPath));
5173
+ } catch {
5174
+ continue;
5175
+ }
5176
+ for (const pkg2 of candidatePackages) {
5177
+ const current = index.get(pkg2);
5178
+ if (!current) continue;
5179
+ const escaped = pkg2.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5180
+ const importRegex = new RegExp(`(?:from\\s+['"]${escaped}['"]|require\\(\\s*['"]${escaped}['"]\\s*\\)|import\\(\\s*['"]${escaped}['"]\\s*\\))`, "g");
5181
+ const importMatches = content.match(importRegex) ?? [];
5182
+ if (importMatches.length > 0) {
5183
+ current.importSites += importMatches.length;
5184
+ current.filesTouched += 1;
5185
+ }
5186
+ const playbook = PACKAGE_PLAYBOOKS[pkg2];
5187
+ if (playbook) {
5188
+ for (const pattern of playbook.usagePatterns) {
5189
+ const matches = content.match(new RegExp(pattern.source, "g"));
5190
+ if (matches) current.patternHits += matches.length;
5191
+ }
5192
+ }
5193
+ }
5194
+ }
5195
+ return index;
5196
+ }
5197
+ async function scanBreakingChangeExposure(projects, rootDir, fileCache) {
5233
5198
  const deprecated = /* @__PURE__ */ new Set();
5234
5199
  const legacyPolyfills = /* @__PURE__ */ new Set();
5235
5200
  let peerConflictsDetected = false;
5236
- const allDeps = /* @__PURE__ */ new Set();
5237
5201
  for (const project of projects) {
5238
5202
  for (const dep of project.dependencies) {
5239
- allDeps.add(dep.package);
5240
- if (DEPRECATED_PACKAGES2.has(dep.package)) {
5241
- deprecated.add(dep.package);
5242
- }
5243
- if (LEGACY_POLYFILLS.has(dep.package)) {
5244
- legacyPolyfills.add(dep.package);
5245
- }
5246
- if (dep.section === "peerDependencies" && dep.majorsBehind !== null && dep.majorsBehind >= 2) {
5247
- peerConflictsDetected = true;
5248
- }
5203
+ if (DEPRECATED_PACKAGES2.has(dep.package)) deprecated.add(dep.package);
5204
+ if (LEGACY_POLYFILLS.has(dep.package)) legacyPolyfills.add(dep.package);
5205
+ if (dep.section === "peerDependencies" && dep.majorsBehind !== null && dep.majorsBehind >= 2) peerConflictsDetected = true;
5249
5206
  }
5250
5207
  }
5251
5208
  let score = 0;
@@ -5253,17 +5210,94 @@ function scanBreakingChangeExposure(projects) {
5253
5210
  score += Math.min(legacyPolyfills.size * 5, 30);
5254
5211
  score += peerConflictsDetected ? 20 : 0;
5255
5212
  score = Math.min(score, 100);
5213
+ const projectIntelligence = [];
5214
+ const solutionRollup = /* @__PURE__ */ new Map();
5215
+ let majorPackageCount = 0;
5216
+ let manualHotspots = 0;
5217
+ let codemodCandidates = 0;
5218
+ for (const project of projects) {
5219
+ const majorDeps = project.dependencies.filter((d) => (d.majorsBehind ?? 0) >= 1 && d.latestStable);
5220
+ if (majorDeps.length === 0) {
5221
+ projectIntelligence.push({
5222
+ project: project.name,
5223
+ projectPath: project.path,
5224
+ packages: [],
5225
+ recommendation: "do-nothing"
5226
+ });
5227
+ continue;
5228
+ }
5229
+ const usageIndex = await buildProjectUsageIndex(project, rootDir, fileCache, majorDeps.map((d) => d.package));
5230
+ const packages = majorDeps.map((dep) => {
5231
+ const currentVersion = resolveCurrentVersion(dep);
5232
+ const targetVersion = dep.latestStable;
5233
+ const currentMajor = normalizeMajor(currentVersion);
5234
+ const targetMajor = normalizeMajor(targetVersion);
5235
+ const interimMajors = currentMajor !== null && targetMajor !== null ? listInterimMajorTargets(currentMajor, targetMajor) : [];
5236
+ const usage = usageIndex.get(dep.package) ?? { importSites: 0, filesTouched: 0, patternHits: 0 };
5237
+ const fileCount = Math.max(1, project.fileCount ?? 1);
5238
+ const touchedPercent = Math.min(100, Math.round((usage.filesTouched + usage.patternHits) / fileCount * 100));
5239
+ const playbook = PACKAGE_PLAYBOOKS[dep.package];
5240
+ const impactedFeatures = playbook?.impactedFeatures ?? ["Public API usage patterns", "Configuration surface", "Runtime compatibility expectations"];
5241
+ const automatable = playbook?.automation ?? (usage.patternHits > 0 ? "manual" : "deterministic-recipe");
5242
+ majorPackageCount++;
5243
+ if (automatable === "manual") manualHotspots++;
5244
+ if (automatable === "codemod-available") codemodCandidates++;
5245
+ return {
5246
+ package: dep.package,
5247
+ currentVersion,
5248
+ targetVersion,
5249
+ majorJumpCount: dep.majorsBehind ?? 0,
5250
+ interimMajors,
5251
+ releaseNoteSources: ["GitHub Releases", "Repository tags", "CHANGELOG.md"],
5252
+ parsedSignals: [...BREAKING_SIGNAL_PHRASES],
5253
+ impactedFeatures,
5254
+ usage: {
5255
+ importSites: usage.importSites,
5256
+ filesTouchedEstimate: usage.filesTouched,
5257
+ functionsTouchedEstimate: usage.patternHits,
5258
+ touchedPercent
5259
+ },
5260
+ automatable,
5261
+ codemod: playbook?.codemod
5262
+ };
5263
+ });
5264
+ const rec = detectDecision(packages.length, packages.filter((p) => p.automatable === "manual").length, packages.filter((p) => p.automatable === "codemod-available").length);
5265
+ projectIntelligence.push({
5266
+ project: project.name,
5267
+ projectPath: project.path,
5268
+ packages,
5269
+ recommendation: rec
5270
+ });
5271
+ if (project.solutionId) {
5272
+ const key = project.solutionId;
5273
+ const existing = solutionRollup.get(key) ?? {
5274
+ solutionId: key,
5275
+ solutionName: project.solutionName ?? key,
5276
+ projectCount: 0,
5277
+ majorPackages: 0,
5278
+ recommendation: "do-nothing"
5279
+ };
5280
+ existing.projectCount += 1;
5281
+ existing.majorPackages += packages.length;
5282
+ existing.recommendation = detectDecision(existing.majorPackages, manualHotspots, codemodCandidates);
5283
+ solutionRollup.set(key, existing);
5284
+ }
5285
+ }
5286
+ const overallRecommendation = detectDecision(majorPackageCount, manualHotspots, codemodCandidates);
5256
5287
  return {
5257
5288
  deprecatedPackages: [...deprecated].sort(),
5258
5289
  legacyPolyfills: [...legacyPolyfills].sort(),
5259
5290
  peerConflictsDetected,
5260
- exposureScore: score
5291
+ exposureScore: score,
5292
+ projectIntelligence,
5293
+ solutionIntelligence: [...solutionRollup.values()],
5294
+ overallRecommendation
5261
5295
  };
5262
5296
  }
5263
5297
 
5264
5298
  // src/scanners/file-hotspots.ts
5265
5299
  import * as fs4 from "fs/promises";
5266
- import * as path16 from "path";
5300
+ import * as path17 from "path";
5267
5301
  var SKIP_DIRS = /* @__PURE__ */ new Set([
5268
5302
  "node_modules",
5269
5303
  ".git",
@@ -5308,9 +5342,9 @@ async function scanFileHotspots(rootDir, cache) {
5308
5342
  const entries = await cache.walkDir(rootDir);
5309
5343
  for (const entry of entries) {
5310
5344
  if (!entry.isFile) continue;
5311
- const ext = path16.extname(entry.name).toLowerCase();
5345
+ const ext = path17.extname(entry.name).toLowerCase();
5312
5346
  if (SKIP_EXTENSIONS.has(ext)) continue;
5313
- const depth = entry.relPath.split(path16.sep).length - 1;
5347
+ const depth = entry.relPath.split(path17.sep).length - 1;
5314
5348
  if (depth > maxDepth) maxDepth = depth;
5315
5349
  extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
5316
5350
  try {
@@ -5339,15 +5373,15 @@ async function scanFileHotspots(rootDir, cache) {
5339
5373
  for (const e of entries) {
5340
5374
  if (e.isDirectory) {
5341
5375
  if (SKIP_DIRS.has(e.name)) continue;
5342
- await walk(path16.join(dir, e.name), depth + 1);
5376
+ await walk(path17.join(dir, e.name), depth + 1);
5343
5377
  } else if (e.isFile) {
5344
- const ext = path16.extname(e.name).toLowerCase();
5378
+ const ext = path17.extname(e.name).toLowerCase();
5345
5379
  if (SKIP_EXTENSIONS.has(ext)) continue;
5346
5380
  extensionCounts[ext] = (extensionCounts[ext] ?? 0) + 1;
5347
5381
  try {
5348
- const stat3 = await fs4.stat(path16.join(dir, e.name));
5382
+ const stat3 = await fs4.stat(path17.join(dir, e.name));
5349
5383
  allFiles.push({
5350
- path: path16.relative(rootDir, path16.join(dir, e.name)),
5384
+ path: path17.relative(rootDir, path17.join(dir, e.name)),
5351
5385
  bytes: stat3.size
5352
5386
  });
5353
5387
  } catch {
@@ -5370,7 +5404,7 @@ async function scanFileHotspots(rootDir, cache) {
5370
5404
  }
5371
5405
 
5372
5406
  // src/scanners/security-posture.ts
5373
- import * as path17 from "path";
5407
+ import * as path18 from "path";
5374
5408
  var LOCKFILES = {
5375
5409
  "pnpm-lock.yaml": "pnpm",
5376
5410
  "package-lock.json": "npm",
@@ -5391,14 +5425,14 @@ async function scanSecurityPosture(rootDir, cache) {
5391
5425
  const _readTextFile = cache ? (p) => cache.readTextFile(p) : readTextFile;
5392
5426
  const foundLockfiles = [];
5393
5427
  for (const [file, type] of Object.entries(LOCKFILES)) {
5394
- if (await _pathExists(path17.join(rootDir, file))) {
5428
+ if (await _pathExists(path18.join(rootDir, file))) {
5395
5429
  foundLockfiles.push(type);
5396
5430
  }
5397
5431
  }
5398
5432
  result.lockfilePresent = foundLockfiles.length > 0;
5399
5433
  result.multipleLockfileTypes = foundLockfiles.length > 1;
5400
5434
  result.lockfileTypes = foundLockfiles.sort();
5401
- const gitignorePath = path17.join(rootDir, ".gitignore");
5435
+ const gitignorePath = path18.join(rootDir, ".gitignore");
5402
5436
  if (await _pathExists(gitignorePath)) {
5403
5437
  try {
5404
5438
  const content = await _readTextFile(gitignorePath);
@@ -5413,7 +5447,7 @@ async function scanSecurityPosture(rootDir, cache) {
5413
5447
  }
5414
5448
  }
5415
5449
  for (const envFile of [".env", ".env.local", ".env.development", ".env.production"]) {
5416
- if (await _pathExists(path17.join(rootDir, envFile))) {
5450
+ if (await _pathExists(path18.join(rootDir, envFile))) {
5417
5451
  if (!result.gitignoreCoversEnv) {
5418
5452
  result.envFilesTracked = true;
5419
5453
  break;
@@ -5425,7 +5459,7 @@ async function scanSecurityPosture(rootDir, cache) {
5425
5459
 
5426
5460
  // src/scanners/security-scanners.ts
5427
5461
  import { spawn as spawn3 } from "child_process";
5428
- import * as path18 from "path";
5462
+ import * as path19 from "path";
5429
5463
  var TOOL_MATRIX = [
5430
5464
  { key: "semgrep", category: "sast", command: "semgrep", versionArgs: ["--version"], minRecommendedVersion: "1.75.0" },
5431
5465
  { key: "gitleaks", category: "secrets", command: "gitleaks", versionArgs: ["version"], minRecommendedVersion: "8.20.0" },
@@ -5520,7 +5554,7 @@ async function detectSecretHeuristics(rootDir, cache) {
5520
5554
  const findings = [];
5521
5555
  for (const entry of entries) {
5522
5556
  if (!entry.isFile) continue;
5523
- const ext = path18.extname(entry.name).toLowerCase();
5557
+ const ext = path19.extname(entry.name).toLowerCase();
5524
5558
  if (ext && [".png", ".jpg", ".jpeg", ".gif", ".zip", ".pdf"].includes(ext)) continue;
5525
5559
  const content = await cache.readTextFile(entry.absPath);
5526
5560
  if (!content || content.length > 3e5) continue;
@@ -5543,9 +5577,9 @@ async function scanSecurityScanners(rootDir, cache, runner = defaultRunner) {
5543
5577
  const [semgrep, gitleaks, trufflehog] = await Promise.all(TOOL_MATRIX.map((tool) => assessTool(tool, runner)));
5544
5578
  const heuristicFindings = await detectSecretHeuristics(rootDir, cache);
5545
5579
  const configFiles = {
5546
- semgrep: await cache.pathExists(path18.join(rootDir, ".semgrep.yml")) || await cache.pathExists(path18.join(rootDir, ".semgrep.yaml")),
5547
- gitleaks: await cache.pathExists(path18.join(rootDir, ".gitleaks.toml")),
5548
- trufflehog: await cache.pathExists(path18.join(rootDir, ".trufflehog.yml")) || await cache.pathExists(path18.join(rootDir, ".trufflehog.yaml"))
5580
+ semgrep: await cache.pathExists(path19.join(rootDir, ".semgrep.yml")) || await cache.pathExists(path19.join(rootDir, ".semgrep.yaml")),
5581
+ gitleaks: await cache.pathExists(path19.join(rootDir, ".gitleaks.toml")),
5582
+ trufflehog: await cache.pathExists(path19.join(rootDir, ".trufflehog.yml")) || await cache.pathExists(path19.join(rootDir, ".trufflehog.yaml"))
5549
5583
  };
5550
5584
  return {
5551
5585
  semgrep,
@@ -5970,7 +6004,7 @@ function scanServiceDependencies(projects) {
5970
6004
  }
5971
6005
 
5972
6006
  // src/scanners/architecture.ts
5973
- import * as path19 from "path";
6007
+ import * as path20 from "path";
5974
6008
  import * as fs5 from "fs/promises";
5975
6009
  var ARCHETYPE_SIGNALS = [
5976
6010
  // Meta-frameworks (highest priority — they imply routing patterns)
@@ -6269,9 +6303,9 @@ async function walkSourceFiles(rootDir, cache) {
6269
6303
  const entries = await cache.walkDir(rootDir);
6270
6304
  return entries.filter((e) => {
6271
6305
  if (!e.isFile) return false;
6272
- const name = path19.basename(e.absPath);
6306
+ const name = path20.basename(e.absPath);
6273
6307
  if (name.startsWith(".") && name !== ".") return false;
6274
- const ext = path19.extname(name);
6308
+ const ext = path20.extname(name);
6275
6309
  return SOURCE_EXTENSIONS.has(ext);
6276
6310
  }).map((e) => e.relPath);
6277
6311
  }
@@ -6285,15 +6319,15 @@ async function walkSourceFiles(rootDir, cache) {
6285
6319
  }
6286
6320
  for (const entry of entries) {
6287
6321
  if (entry.name.startsWith(".") && entry.name !== ".") continue;
6288
- const fullPath = path19.join(dir, entry.name);
6322
+ const fullPath = path20.join(dir, entry.name);
6289
6323
  if (entry.isDirectory()) {
6290
6324
  if (!IGNORE_DIRS.has(entry.name)) {
6291
6325
  await walk(fullPath);
6292
6326
  }
6293
6327
  } else if (entry.isFile()) {
6294
- const ext = path19.extname(entry.name);
6328
+ const ext = path20.extname(entry.name);
6295
6329
  if (SOURCE_EXTENSIONS.has(ext)) {
6296
- files.push(path19.relative(rootDir, fullPath));
6330
+ files.push(path20.relative(rootDir, fullPath));
6297
6331
  }
6298
6332
  }
6299
6333
  }
@@ -6317,7 +6351,7 @@ function classifyFile(filePath, archetype) {
6317
6351
  }
6318
6352
  }
6319
6353
  if (!bestMatch || bestMatch.confidence < 0.7) {
6320
- const baseName = path19.basename(filePath, path19.extname(filePath));
6354
+ const baseName = path20.basename(filePath, path20.extname(filePath));
6321
6355
  const cleanBase = baseName.replace(/\.(test|spec)$/, "");
6322
6356
  for (const rule of SUFFIX_RULES) {
6323
6357
  if (cleanBase.endsWith(rule.suffix)) {
@@ -6426,7 +6460,7 @@ function generateLayerFlowMermaid(layers) {
6426
6460
  return lines.join("\n");
6427
6461
  }
6428
6462
  async function buildProjectArchitectureMermaid(rootDir, project, archetype, cache) {
6429
- const projectRoot = path19.resolve(rootDir, project.path || ".");
6463
+ const projectRoot = path20.resolve(rootDir, project.path || ".");
6430
6464
  const allFiles = await walkSourceFiles(projectRoot, cache);
6431
6465
  const layerSet = /* @__PURE__ */ new Set();
6432
6466
  for (const rel of allFiles) {
@@ -6552,7 +6586,7 @@ async function scanArchitecture(rootDir, projects, tooling, services, cache) {
6552
6586
  }
6553
6587
 
6554
6588
  // src/scanners/code-quality.ts
6555
- import * as path20 from "path";
6589
+ import * as path21 from "path";
6556
6590
  import * as ts from "typescript";
6557
6591
  var SOURCE_EXTENSIONS2 = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
6558
6592
  var DEFAULT_RESULT = {
@@ -6583,9 +6617,9 @@ async function scanCodeQuality(rootDir, cache) {
6583
6617
  continue;
6584
6618
  }
6585
6619
  if (!raw.trim()) continue;
6586
- const rel = normalizeModuleId(path20.relative(rootDir, filePath));
6620
+ const rel = normalizeModuleId(path21.relative(rootDir, filePath));
6587
6621
  const source = ts.createSourceFile(filePath, raw, ts.ScriptTarget.Latest, true);
6588
- const imports = collectLocalImports(source, path20.dirname(filePath), rootDir);
6622
+ const imports = collectLocalImports(source, path21.dirname(filePath), rootDir);
6589
6623
  depGraph.set(rel, imports);
6590
6624
  const fileMetrics = computeFileMetrics(source, raw);
6591
6625
  totalFunctions += fileMetrics.functionsAnalyzed;
@@ -6619,9 +6653,9 @@ async function scanCodeQuality(rootDir, cache) {
6619
6653
  async function findSourceFiles(rootDir, cache) {
6620
6654
  if (cache) {
6621
6655
  const entries = await cache.walkDir(rootDir);
6622
- return entries.filter((entry) => entry.isFile && SOURCE_EXTENSIONS2.has(path20.extname(entry.name).toLowerCase())).map((entry) => entry.absPath);
6656
+ return entries.filter((entry) => entry.isFile && SOURCE_EXTENSIONS2.has(path21.extname(entry.name).toLowerCase())).map((entry) => entry.absPath);
6623
6657
  }
6624
- const files = await findFiles(rootDir, (name) => SOURCE_EXTENSIONS2.has(path20.extname(name).toLowerCase()));
6658
+ const files = await findFiles(rootDir, (name) => SOURCE_EXTENSIONS2.has(path21.extname(name).toLowerCase()));
6625
6659
  return files;
6626
6660
  }
6627
6661
  function collectLocalImports(source, fileDir, rootDir) {
@@ -6642,8 +6676,8 @@ function collectLocalImports(source, fileDir, rootDir) {
6642
6676
  }
6643
6677
  function resolveLocalImport(specifier, fileDir, rootDir) {
6644
6678
  if (!specifier.startsWith(".")) return null;
6645
- const rawTarget = path20.resolve(fileDir, specifier);
6646
- const normalized = path20.relative(rootDir, rawTarget).replace(/\\/g, "/");
6679
+ const rawTarget = path21.resolve(fileDir, specifier);
6680
+ const normalized = path21.relative(rootDir, rawTarget).replace(/\\/g, "/");
6647
6681
  if (!normalized || normalized.startsWith("..")) return null;
6648
6682
  return normalizeModuleId(normalized);
6649
6683
  }
@@ -6785,7 +6819,7 @@ function visitEach(node, cb) {
6785
6819
 
6786
6820
  // src/scanners/owasp-category-mapping.ts
6787
6821
  import { spawn as spawn4 } from "child_process";
6788
- import * as path21 from "path";
6822
+ import * as path22 from "path";
6789
6823
  var OWASP_CONFIG = "p/owasp-top-ten";
6790
6824
  var DEFAULT_EXTENSIONS = /* @__PURE__ */ new Set([
6791
6825
  ".js",
@@ -6864,7 +6898,7 @@ function parseFindings(results, rootDir) {
6864
6898
  const metadata = r.extra?.metadata;
6865
6899
  return {
6866
6900
  ruleId: r.check_id ?? "unknown",
6867
- path: r.path ? path21.relative(rootDir, path21.resolve(rootDir, r.path)) : "",
6901
+ path: r.path ? path22.relative(rootDir, path22.resolve(rootDir, r.path)) : "",
6868
6902
  line: r.start?.line ?? 1,
6869
6903
  endLine: r.end?.line,
6870
6904
  message: r.extra?.message ?? "Potential security issue",
@@ -6924,7 +6958,7 @@ async function scanOwaspCategoryMapping(rootDir, cache, options = {}, runner = r
6924
6958
  }
6925
6959
  }
6926
6960
  const entries = cache ? await cache.walkDir(rootDir) : [];
6927
- const files = entries.filter((e) => e.isFile && DEFAULT_EXTENSIONS.has(path21.extname(e.name).toLowerCase()));
6961
+ const files = entries.filter((e) => e.isFile && DEFAULT_EXTENSIONS.has(path22.extname(e.name).toLowerCase()));
6928
6962
  const findings = [];
6929
6963
  const errors = [];
6930
6964
  let scannedFiles = 0;
@@ -7372,17 +7406,17 @@ async function discoverSolutions(rootDir, fileCache) {
7372
7406
  for (const solutionFile of solutionFiles) {
7373
7407
  try {
7374
7408
  const content = await fileCache.readTextFile(solutionFile);
7375
- const dir = path22.dirname(solutionFile);
7376
- const relSolutionPath = path22.relative(rootDir, solutionFile).replace(/\\/g, "/");
7409
+ const dir = path23.dirname(solutionFile);
7410
+ const relSolutionPath = path23.relative(rootDir, solutionFile).replace(/\\/g, "/");
7377
7411
  const projectPaths = /* @__PURE__ */ new Set();
7378
7412
  const projectRegex = /Project\("[^"]*"\)\s*=\s*"([^"]*)",\s*"([^"]+\.csproj)"/g;
7379
7413
  let match;
7380
7414
  while ((match = projectRegex.exec(content)) !== null) {
7381
7415
  const projectRelative = match[2];
7382
- const absProjectPath = path22.resolve(dir, projectRelative.replace(/\\/g, "/"));
7383
- projectPaths.add(path22.relative(rootDir, absProjectPath).replace(/\\/g, "/"));
7416
+ const absProjectPath = path23.resolve(dir, projectRelative.replace(/\\/g, "/"));
7417
+ projectPaths.add(path23.relative(rootDir, absProjectPath).replace(/\\/g, "/"));
7384
7418
  }
7385
- const solutionName = path22.basename(solutionFile, path22.extname(solutionFile));
7419
+ const solutionName = path23.basename(solutionFile, path23.extname(solutionFile));
7386
7420
  parsed.push({
7387
7421
  path: relSolutionPath,
7388
7422
  name: solutionName,
@@ -7575,7 +7609,7 @@ async function runScan(rootDir, opts) {
7575
7609
  project.drift = computeDriftScore([project]);
7576
7610
  project.projectId = computeProjectId(project.path, project.name, workspaceId);
7577
7611
  }
7578
- const solutionsManifestPath = path22.join(rootDir, ".vibgrate", "solutions.json");
7612
+ const solutionsManifestPath = path23.join(rootDir, ".vibgrate", "solutions.json");
7579
7613
  const persistedSolutionIds = /* @__PURE__ */ new Map();
7580
7614
  if (await pathExists(solutionsManifestPath)) {
7581
7615
  try {
@@ -7650,8 +7684,8 @@ async function runScan(rootDir, opts) {
7650
7684
  if (scannerPolicy.breakingChangeExposure && scanners?.breakingChangeExposure?.enabled !== false) {
7651
7685
  progress.startStep("breaking");
7652
7686
  scannerTasks.push(
7653
- Promise.resolve().then(() => {
7654
- extended.breakingChangeExposure = scanBreakingChangeExposure(allProjects);
7687
+ Promise.resolve().then(async () => {
7688
+ extended.breakingChangeExposure = await scanBreakingChangeExposure(allProjects, rootDir, fileCache);
7655
7689
  const bc = extended.breakingChangeExposure;
7656
7690
  const bcTotal = bc.deprecatedPackages.length + bc.legacyPolyfills.length;
7657
7691
  progress.completeStep(
@@ -7885,7 +7919,7 @@ async function runScan(rootDir, opts) {
7885
7919
  schemaVersion: "1.0",
7886
7920
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7887
7921
  vibgrateVersion: VERSION,
7888
- rootPath: path22.basename(rootDir),
7922
+ rootPath: path23.basename(rootDir),
7889
7923
  ...vcs.type !== "unknown" ? { vcs } : {},
7890
7924
  repository,
7891
7925
  projects: allProjects,
@@ -7899,7 +7933,7 @@ async function runScan(rootDir, opts) {
7899
7933
  relationshipDiagram
7900
7934
  };
7901
7935
  if (opts.baseline) {
7902
- const baselinePath = path22.resolve(opts.baseline);
7936
+ const baselinePath = path23.resolve(opts.baseline);
7903
7937
  if (await pathExists(baselinePath)) {
7904
7938
  try {
7905
7939
  const baseline = await readJsonFile(baselinePath);
@@ -7911,10 +7945,10 @@ async function runScan(rootDir, opts) {
7911
7945
  }
7912
7946
  }
7913
7947
  if (!opts.noLocalArtifacts && !maxPrivacyMode) {
7914
- const vibgrateDir = path22.join(rootDir, ".vibgrate");
7948
+ const vibgrateDir = path23.join(rootDir, ".vibgrate");
7915
7949
  await ensureDir(vibgrateDir);
7916
- await writeJsonFile(path22.join(vibgrateDir, "scan_result.json"), artifact);
7917
- await writeJsonFile(path22.join(vibgrateDir, "solutions.json"), {
7950
+ await writeJsonFile(path23.join(vibgrateDir, "scan_result.json"), artifact);
7951
+ await writeJsonFile(path23.join(vibgrateDir, "solutions.json"), {
7918
7952
  scannedAt: artifact.timestamp,
7919
7953
  solutions: solutions.map((solution) => ({
7920
7954
  solutionId: solution.solutionId,
@@ -7935,10 +7969,10 @@ async function runScan(rootDir, opts) {
7935
7969
  if (!opts.noLocalArtifacts && !maxPrivacyMode) {
7936
7970
  for (const project of allProjects) {
7937
7971
  if (project.drift && project.path) {
7938
- const projectDir = path22.resolve(rootDir, project.path);
7939
- const projectVibgrateDir = path22.join(projectDir, ".vibgrate");
7972
+ const projectDir = path23.resolve(rootDir, project.path);
7973
+ const projectVibgrateDir = path23.join(projectDir, ".vibgrate");
7940
7974
  await ensureDir(projectVibgrateDir);
7941
- await writeJsonFile(path22.join(projectVibgrateDir, "project_score.json"), {
7975
+ await writeJsonFile(path23.join(projectVibgrateDir, "project_score.json"), {
7942
7976
  projectId: project.projectId,
7943
7977
  name: project.name,
7944
7978
  type: project.type,
@@ -7958,7 +7992,7 @@ async function runScan(rootDir, opts) {
7958
7992
  if (opts.format === "json") {
7959
7993
  const jsonStr = JSON.stringify(artifact, null, 2);
7960
7994
  if (opts.out) {
7961
- await writeTextFile(path22.resolve(opts.out), jsonStr);
7995
+ await writeTextFile(path23.resolve(opts.out), jsonStr);
7962
7996
  console.log(chalk6.green("\u2714") + ` JSON written to ${opts.out}`);
7963
7997
  } else {
7964
7998
  console.log(jsonStr);
@@ -7967,7 +8001,7 @@ async function runScan(rootDir, opts) {
7967
8001
  const sarif = formatSarif(artifact);
7968
8002
  const sarifStr = JSON.stringify(sarif, null, 2);
7969
8003
  if (opts.out) {
7970
- await writeTextFile(path22.resolve(opts.out), sarifStr);
8004
+ await writeTextFile(path23.resolve(opts.out), sarifStr);
7971
8005
  console.log(chalk6.green("\u2714") + ` SARIF written to ${opts.out}`);
7972
8006
  } else {
7973
8007
  console.log(sarifStr);
@@ -7976,20 +8010,20 @@ async function runScan(rootDir, opts) {
7976
8010
  const markdown = formatMarkdown(artifact);
7977
8011
  console.log(markdown);
7978
8012
  if (opts.out) {
7979
- await writeTextFile(path22.resolve(opts.out), markdown);
8013
+ await writeTextFile(path23.resolve(opts.out), markdown);
7980
8014
  }
7981
8015
  } else {
7982
8016
  const text = formatText(artifact);
7983
8017
  console.log(text);
7984
8018
  if (opts.out) {
7985
- await writeTextFile(path22.resolve(opts.out), text);
8019
+ await writeTextFile(path23.resolve(opts.out), text);
7986
8020
  }
7987
8021
  }
7988
8022
  return artifact;
7989
8023
  }
7990
8024
  async function buildRepositoryInfo(rootDir, remoteUrl, ciSystems) {
7991
- const packageJsonPath = path22.join(rootDir, "package.json");
7992
- let name = path22.basename(rootDir);
8025
+ const packageJsonPath = path23.join(rootDir, "package.json");
8026
+ let name = path23.basename(rootDir);
7993
8027
  let version;
7994
8028
  if (await pathExists(packageJsonPath)) {
7995
8029
  try {
@@ -8081,7 +8115,7 @@ function parseNonNegativeNumber(value, label) {
8081
8115
  return parsed;
8082
8116
  }
8083
8117
  var scanCommand = new Command3("scan").description("Scan a project for upgrade drift").argument("[path]", "Path to scan", ".").option("--out <file>", "Output file path").option("--format <format>", "Output format (text|json|sarif|md)", "text").option("--fail-on <level>", "Fail on warn or error").option("--baseline <file>", "Compare against baseline").option("--changed-only", "Only scan changed files").option("--concurrency <n>", "Max concurrent npm calls", "8").option("--push", "Auto-push results to Vibgrate API after scan").option("--dsn <dsn>", "DSN token for push (or use VIBGRATE_DSN env)").option("--region <region>", "Override data residency region for push (us, eu)").option("--strict", "Fail on push errors").option("--install-tools", "Auto-install missing security scanners via Homebrew").option("--ui-purpose", "Enable optional UI purpose evidence extraction (slower)").option("--no-local-artifacts", "Do not write .vibgrate JSON artifacts to disk").option("--max-privacy", "Enable strongest privacy mode (minimal scanners, no local artifacts)").option("--offline", "Run without network calls; do not upload results").option("--package-manifest <file>", "Use local package-version manifest JSON/ZIP (for offline mode)").option("--drift-budget <score>", "Fail if drift score is above budget (0-100)").option("--drift-worsening <percent>", "Fail if drift worsens by more than % since baseline").action(async (targetPath, opts) => {
8084
- const rootDir = path22.resolve(targetPath);
8118
+ const rootDir = path23.resolve(targetPath);
8085
8119
  if (!await pathExists(rootDir)) {
8086
8120
  console.error(chalk6.red(`Path does not exist: ${rootDir}`));
8087
8121
  process.exit(1);
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  baselineCommand
4
- } from "./chunk-TCYJSLL2.js";
4
+ } from "./chunk-CQMW6PVB.js";
5
5
  import {
6
6
  VERSION,
7
7
  dsnCommand,
@@ -10,7 +10,7 @@ import {
10
10
  pushCommand,
11
11
  scanCommand,
12
12
  writeDefaultConfig
13
- } from "./chunk-TYAGUEXG.js";
13
+ } from "./chunk-PQEUNAVK.js";
14
14
  import {
15
15
  ensureDir,
16
16
  pathExists,
@@ -19,8 +19,8 @@ import {
19
19
  } from "./chunk-RNVZIZNL.js";
20
20
 
21
21
  // src/cli.ts
22
- import { Command as Command5 } from "commander";
23
- import chalk5 from "chalk";
22
+ import { Command as Command6 } from "commander";
23
+ import chalk6 from "chalk";
24
24
 
25
25
  // src/commands/init.ts
26
26
  import * as path from "path";
@@ -39,7 +39,7 @@ var initCommand = new Command("init").description("Initialize vibgrate in a proj
39
39
  console.log(chalk.green("\u2714") + ` Created ${chalk.bold("vibgrate.config.ts")}`);
40
40
  }
41
41
  if (opts.baseline) {
42
- const { runBaseline } = await import("./baseline-FRBISJ66.js");
42
+ const { runBaseline } = await import("./baseline-5ZOILNXB.js");
43
43
  await runBaseline(rootDir);
44
44
  }
45
45
  console.log("");
@@ -409,9 +409,239 @@ var deltaCommand = new Command4("delta").description("Show SBOM delta between tw
409
409
  });
410
410
  var sbomCommand = new Command4("sbom").description("SBOM export and delta reports for dependency drift tracking").addCommand(exportCommand).addCommand(deltaCommand);
411
411
 
412
+ // src/commands/help.ts
413
+ import { Command as Command5 } from "commander";
414
+ import chalk5 from "chalk";
415
+ var HELP_URL = "https://vibgrate.com/help";
416
+ function printFooter() {
417
+ console.log("");
418
+ console.log(chalk5.dim(`See ${HELP_URL} for more guidance`));
419
+ }
420
+ var detailedHelp = {
421
+ scan: () => {
422
+ console.log("");
423
+ console.log(chalk5.bold.underline("vibgrate scan") + chalk5.dim(" \u2014 Scan a project for upgrade drift"));
424
+ console.log("");
425
+ console.log(chalk5.bold("Usage:"));
426
+ console.log(" vibgrate scan [path] [options]");
427
+ console.log("");
428
+ console.log(chalk5.bold("Arguments:"));
429
+ console.log(` ${chalk5.cyan("[path]")} Path to scan (default: current directory)`);
430
+ console.log("");
431
+ console.log(chalk5.bold("Output options:"));
432
+ console.log(` ${chalk5.cyan("--format <format>")} Output format: ${chalk5.white("text")} | json | sarif | md (default: text)`);
433
+ console.log(` ${chalk5.cyan("--out <file>")} Write output to a file instead of stdout`);
434
+ console.log("");
435
+ console.log(chalk5.bold("Baseline & gating:"));
436
+ console.log(` ${chalk5.cyan("--baseline <file>")} Compare results against a saved baseline`);
437
+ console.log(` ${chalk5.cyan("--drift-budget <score>")} Fail if drift score exceeds this value (0\u2013100)`);
438
+ console.log(` ${chalk5.cyan("--drift-worsening <percent>")} Fail if drift worsens by more than % since baseline`);
439
+ console.log(` ${chalk5.cyan("--fail-on <level>")} Fail exit code on warn or error findings`);
440
+ console.log("");
441
+ console.log(chalk5.bold("Performance:"));
442
+ console.log(` ${chalk5.cyan("--concurrency <n>")} Max concurrent registry calls (default: 8)`);
443
+ console.log(` ${chalk5.cyan("--changed-only")} Only scan files changed since last git commit`);
444
+ console.log("");
445
+ console.log(chalk5.bold("Privacy & offline:"));
446
+ console.log(` ${chalk5.cyan("--offline")} Run without any network calls; skip result upload`);
447
+ console.log(` ${chalk5.cyan("--package-manifest <file>")} Use a local package-version manifest (JSON or ZIP) for offline mode`);
448
+ console.log(` ${chalk5.cyan("--no-local-artifacts")} Do not write .vibgrate JSON artifacts to disk`);
449
+ console.log(` ${chalk5.cyan("--max-privacy")} Strongest privacy mode: minimal scanners + no local artifacts`);
450
+ console.log("");
451
+ console.log(chalk5.bold("Tooling:"));
452
+ console.log(` ${chalk5.cyan("--install-tools")} Auto-install missing security scanners via Homebrew`);
453
+ console.log(` ${chalk5.cyan("--ui-purpose")} Enable UI purpose evidence extraction (slower)`);
454
+ console.log("");
455
+ console.log(chalk5.bold("Uploading results:"));
456
+ console.log(` ${chalk5.cyan("--push")} Auto-push results to Vibgrate API after scan`);
457
+ console.log(` ${chalk5.cyan("--dsn <dsn>")} DSN token for push (or set VIBGRATE_DSN env var)`);
458
+ console.log(` ${chalk5.cyan("--region <region>")} Data residency region: us | eu (default: us)`);
459
+ console.log(` ${chalk5.cyan("--strict")} Fail if the upload to Vibgrate API fails`);
460
+ console.log("");
461
+ console.log(chalk5.bold("Examples:"));
462
+ console.log(` ${chalk5.dim("# Scan the current directory and display a text report")}`);
463
+ console.log(" vibgrate scan .");
464
+ console.log("");
465
+ console.log(` ${chalk5.dim("# Scan, fail if drift score > 40, and write SARIF for GitHub Actions")}`);
466
+ console.log(" vibgrate scan . --drift-budget 40 --format sarif --out drift.sarif");
467
+ console.log("");
468
+ console.log(` ${chalk5.dim("# Scan and automatically upload results via a DSN")}`);
469
+ console.log(" vibgrate scan . --push --dsn $VIBGRATE_DSN");
470
+ console.log("");
471
+ console.log(` ${chalk5.dim("# Offline scan using a pre-downloaded package manifest")}`);
472
+ console.log(" vibgrate scan . --offline --package-manifest ./manifest.zip");
473
+ },
474
+ init: () => {
475
+ console.log("");
476
+ console.log(chalk5.bold.underline("vibgrate init") + chalk5.dim(" \u2014 Initialise vibgrate in a project directory"));
477
+ console.log("");
478
+ console.log(chalk5.bold("Usage:"));
479
+ console.log(" vibgrate init [path] [options]");
480
+ console.log("");
481
+ console.log(chalk5.bold("Arguments:"));
482
+ console.log(` ${chalk5.cyan("[path]")} Directory to initialise (default: current directory)`);
483
+ console.log("");
484
+ console.log(chalk5.bold("Options:"));
485
+ console.log(` ${chalk5.cyan("--baseline")} Create an initial drift baseline after init`);
486
+ console.log(` ${chalk5.cyan("--yes")} Skip all confirmation prompts`);
487
+ console.log("");
488
+ console.log(chalk5.bold("What it does:"));
489
+ console.log(" \u2022 Creates a .vibgrate/ directory");
490
+ console.log(" \u2022 Writes a vibgrate.config.ts starter config");
491
+ console.log(" \u2022 Optionally runs an initial baseline scan (--baseline)");
492
+ console.log("");
493
+ console.log(chalk5.bold("Examples:"));
494
+ console.log(" vibgrate init");
495
+ console.log(" vibgrate init ./my-project --baseline");
496
+ },
497
+ baseline: () => {
498
+ console.log("");
499
+ console.log(chalk5.bold.underline("vibgrate baseline") + chalk5.dim(" \u2014 Save a drift baseline snapshot"));
500
+ console.log("");
501
+ console.log(chalk5.bold("Usage:"));
502
+ console.log(" vibgrate baseline [path]");
503
+ console.log("");
504
+ console.log(chalk5.bold("Arguments:"));
505
+ console.log(` ${chalk5.cyan("[path]")} Path to baseline (default: current directory)`);
506
+ console.log("");
507
+ console.log(chalk5.bold("What it does:"));
508
+ console.log(" Runs a full scan and saves the result as .vibgrate/baseline.json.");
509
+ console.log(" Future scans can compare against this file using --baseline.");
510
+ console.log("");
511
+ console.log(chalk5.bold("Examples:"));
512
+ console.log(" vibgrate baseline .");
513
+ console.log(" vibgrate scan . --baseline .vibgrate/baseline.json --drift-worsening 10");
514
+ },
515
+ report: () => {
516
+ console.log("");
517
+ console.log(chalk5.bold.underline("vibgrate report") + chalk5.dim(" \u2014 Generate a report from a saved scan artifact"));
518
+ console.log("");
519
+ console.log(chalk5.bold("Usage:"));
520
+ console.log(" vibgrate report [options]");
521
+ console.log("");
522
+ console.log(chalk5.bold("Options:"));
523
+ console.log(` ${chalk5.cyan("--in <file>")} Input artifact file (default: .vibgrate/scan_result.json)`);
524
+ console.log(` ${chalk5.cyan("--format <format>")} Output format: ${chalk5.white("text")} | md | json (default: text)`);
525
+ console.log("");
526
+ console.log(chalk5.bold("Examples:"));
527
+ console.log(" vibgrate report");
528
+ console.log(" vibgrate report --format md > DRIFT-REPORT.md");
529
+ console.log(" vibgrate report --in ./ci/scan_result.json --format json");
530
+ },
531
+ sbom: () => {
532
+ console.log("");
533
+ console.log(chalk5.bold.underline("vibgrate sbom") + chalk5.dim(" \u2014 Export a Software Bill of Materials from a scan artifact"));
534
+ console.log("");
535
+ console.log(chalk5.bold("Usage:"));
536
+ console.log(" vibgrate sbom [options]");
537
+ console.log("");
538
+ console.log(chalk5.bold("Options:"));
539
+ console.log(` ${chalk5.cyan("--in <file>")} Input artifact (default: .vibgrate/scan_result.json)`);
540
+ console.log(` ${chalk5.cyan("--format <format>")} SBOM format: ${chalk5.white("cyclonedx")} | spdx (default: cyclonedx)`);
541
+ console.log(` ${chalk5.cyan("--out <file>")} Write SBOM to file instead of stdout`);
542
+ console.log("");
543
+ console.log(chalk5.bold("Examples:"));
544
+ console.log(" vibgrate sbom --format cyclonedx --out sbom.json");
545
+ console.log(" vibgrate sbom --format spdx --out sbom.spdx.json");
546
+ },
547
+ push: () => {
548
+ console.log("");
549
+ console.log(chalk5.bold.underline("vibgrate push") + chalk5.dim(" \u2014 Upload a scan artifact to the Vibgrate API"));
550
+ console.log("");
551
+ console.log(chalk5.bold("Usage:"));
552
+ console.log(" vibgrate push [options]");
553
+ console.log("");
554
+ console.log(chalk5.bold("Options:"));
555
+ console.log(` ${chalk5.cyan("--dsn <dsn>")} DSN token (or set VIBGRATE_DSN env var)`);
556
+ console.log(` ${chalk5.cyan("--file <file>")} Artifact to upload (default: .vibgrate/scan_result.json)`);
557
+ console.log(` ${chalk5.cyan("--region <region>")} Override data residency region: us | eu`);
558
+ console.log(` ${chalk5.cyan("--strict")} Fail with non-zero exit code on upload error`);
559
+ console.log("");
560
+ console.log(chalk5.bold("Examples:"));
561
+ console.log(" vibgrate push --dsn $VIBGRATE_DSN");
562
+ console.log(" vibgrate push --file ./ci/scan_result.json --strict");
563
+ },
564
+ dsn: () => {
565
+ console.log("");
566
+ console.log(chalk5.bold.underline("vibgrate dsn") + chalk5.dim(" \u2014 Manage DSN tokens for API authentication"));
567
+ console.log("");
568
+ console.log(chalk5.bold("Subcommands:"));
569
+ console.log(` ${chalk5.cyan("vibgrate dsn create")} Generate a new DSN token`);
570
+ console.log("");
571
+ console.log(chalk5.bold("dsn create options:"));
572
+ console.log(` ${chalk5.cyan("--workspace <id>")} Workspace ID ${chalk5.red("(required)")}`);
573
+ console.log(` ${chalk5.cyan("--region <region>")} Data residency region: us | eu (default: us)`);
574
+ console.log(` ${chalk5.cyan("--ingest <url>")} Override ingest API URL`);
575
+ console.log(` ${chalk5.cyan("--write <path>")} Write the DSN to a file (add to .gitignore!)`);
576
+ console.log("");
577
+ console.log(chalk5.bold("Examples:"));
578
+ console.log(" vibgrate dsn create --workspace abc123");
579
+ console.log(" vibgrate dsn create --workspace abc123 --region eu --write .vibgrate/.dsn");
580
+ },
581
+ update: () => {
582
+ console.log("");
583
+ console.log(chalk5.bold.underline("vibgrate update") + chalk5.dim(" \u2014 Update the vibgrate CLI to the latest version"));
584
+ console.log("");
585
+ console.log(chalk5.bold("Usage:"));
586
+ console.log(" vibgrate update [options]");
587
+ console.log("");
588
+ console.log(chalk5.bold("Options:"));
589
+ console.log(` ${chalk5.cyan("--check")} Check for a newer version without installing`);
590
+ console.log(` ${chalk5.cyan("--pm <manager>")} Force a package manager: npm | pnpm | yarn | bun`);
591
+ console.log("");
592
+ console.log(chalk5.bold("Examples:"));
593
+ console.log(" vibgrate update");
594
+ console.log(" vibgrate update --check");
595
+ console.log(" vibgrate update --pm pnpm");
596
+ }
597
+ };
598
+ function printSummaryHelp() {
599
+ console.log("");
600
+ console.log(chalk5.bold("vibgrate") + chalk5.dim(" \u2014 Continuous Drift Intelligence"));
601
+ console.log("");
602
+ console.log(chalk5.bold("Usage:"));
603
+ console.log(" vibgrate <command> [options]");
604
+ console.log(" vibgrate help [command] Show detailed help for a command");
605
+ console.log("");
606
+ console.log(chalk5.bold("Getting started:"));
607
+ console.log(` ${chalk5.cyan("init")} Initialise vibgrate in a project (creates config & .vibgrate/ dir)`);
608
+ console.log("");
609
+ console.log(chalk5.bold("Core scanning:"));
610
+ console.log(` ${chalk5.cyan("scan")} Scan a project for upgrade drift and generate a report`);
611
+ console.log(` ${chalk5.cyan("baseline")} Save a baseline snapshot to compare future scans against`);
612
+ console.log("");
613
+ console.log(chalk5.bold("Reporting & export:"));
614
+ console.log(` ${chalk5.cyan("report")} Re-generate a report from a previously saved scan artifact`);
615
+ console.log(` ${chalk5.cyan("sbom")} Export a Software Bill of Materials (CycloneDX or SPDX)`);
616
+ console.log("");
617
+ console.log(chalk5.bold("CI/CD integration:"));
618
+ console.log(` ${chalk5.cyan("push")} Upload a scan artifact to the Vibgrate API`);
619
+ console.log(` ${chalk5.cyan("dsn")} Create and manage DSN tokens for API authentication`);
620
+ console.log("");
621
+ console.log(chalk5.bold("Maintenance:"));
622
+ console.log(` ${chalk5.cyan("update")} Update the vibgrate CLI to the latest version`);
623
+ console.log("");
624
+ console.log(chalk5.dim("Run") + ` ${chalk5.cyan("vibgrate help <command>")} ` + chalk5.dim("for detailed options, e.g.") + ` ${chalk5.cyan("vibgrate help scan")}`);
625
+ }
626
+ var helpCommand = new Command5("help").description("Show help for vibgrate commands").argument("[command]", "Command to show detailed help for").helpOption(false).action((cmd) => {
627
+ const name = cmd?.toLowerCase().trim();
628
+ if (name && detailedHelp[name]) {
629
+ detailedHelp[name]();
630
+ } else if (name) {
631
+ console.log("");
632
+ console.log(chalk5.red(`Unknown command: ${name}`));
633
+ console.log(chalk5.dim(`Available commands: ${Object.keys(detailedHelp).join(", ")}`));
634
+ printSummaryHelp();
635
+ } else {
636
+ printSummaryHelp();
637
+ }
638
+ printFooter();
639
+ });
640
+
412
641
  // src/cli.ts
413
- var program = new Command5();
414
- program.name("vibgrate").description("Continuous Drift Intelligence for Node & .NET").version(VERSION);
642
+ var program = new Command6();
643
+ program.name("vibgrate").description("Continuous Drift Intelligence").version(VERSION).addHelpText("after", "\nSee https://vibgrate.com/help for more guidance");
644
+ program.addCommand(helpCommand);
415
645
  program.addCommand(initCommand);
416
646
  program.addCommand(scanCommand);
417
647
  program.addCommand(baselineCommand);
@@ -424,8 +654,8 @@ function notifyIfUpdateAvailable() {
424
654
  void checkForUpdate().then((update) => {
425
655
  if (!update?.updateAvailable) return;
426
656
  console.error("");
427
- console.error(chalk5.yellow(` Update available: ${update.current} \u2192 ${update.latest}`));
428
- console.error(chalk5.dim(' Run "vibgrate update" to install the latest version.'));
657
+ console.error(chalk6.yellow(` Update available: ${update.current} \u2192 ${update.latest}`));
658
+ console.error(chalk6.dim(' Run "vibgrate update" to install the latest version.'));
429
659
  console.error("");
430
660
  }).catch(() => {
431
661
  });
package/dist/index.d.ts CHANGED
@@ -287,11 +287,46 @@ interface TsModernityResult {
287
287
  moduleType: 'esm' | 'cjs' | 'mixed' | null;
288
288
  exportsField: boolean;
289
289
  }
290
+ type UpgradeRecommendation = 'do-nothing' | 'upgrade-safely-now' | 'plan-major-upgrade' | 'codemod-available' | 'manual-hotspots';
291
+ interface BreakingChangePackageIntelligence {
292
+ package: string;
293
+ currentVersion: string | null;
294
+ targetVersion: string | null;
295
+ majorJumpCount: number;
296
+ interimMajors: string[];
297
+ releaseNoteSources: string[];
298
+ parsedSignals: string[];
299
+ impactedFeatures: string[];
300
+ usage: {
301
+ importSites: number;
302
+ filesTouchedEstimate: number;
303
+ functionsTouchedEstimate: number;
304
+ touchedPercent: number;
305
+ };
306
+ automatable: 'codemod-available' | 'deterministic-recipe' | 'manual';
307
+ codemod?: string;
308
+ }
309
+ interface BreakingChangeProjectIntelligence {
310
+ project: string;
311
+ projectPath: string;
312
+ packages: BreakingChangePackageIntelligence[];
313
+ recommendation: UpgradeRecommendation;
314
+ }
315
+ interface BreakingChangeSolutionIntelligence {
316
+ solutionId: string;
317
+ solutionName: string;
318
+ projectCount: number;
319
+ majorPackages: number;
320
+ recommendation: UpgradeRecommendation;
321
+ }
290
322
  interface BreakingChangeExposureResult {
291
323
  deprecatedPackages: string[];
292
324
  legacyPolyfills: string[];
293
325
  peerConflictsDetected: boolean;
294
326
  exposureScore: number;
327
+ projectIntelligence: BreakingChangeProjectIntelligence[];
328
+ solutionIntelligence: BreakingChangeSolutionIntelligence[];
329
+ overallRecommendation: UpgradeRecommendation;
295
330
  }
296
331
  interface FileHotspot {
297
332
  path: string;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  formatText,
6
6
  generateFindings,
7
7
  runScan
8
- } from "./chunk-TYAGUEXG.js";
8
+ } from "./chunk-PQEUNAVK.js";
9
9
  import "./chunk-RNVZIZNL.js";
10
10
  export {
11
11
  computeDriftScore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibgrate/cli",
3
- "version": "1.0.52",
3
+ "version": "1.0.54",
4
4
  "description": "CLI for measuring upgrade drift across Node, .NET, Python & Java projects",
5
5
  "type": "module",
6
6
  "bin": {