bun-ready 0.2.3 → 0.3.0

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.
Files changed (3) hide show
  1. package/README.md +178 -5
  2. package/dist/cli.js +1345 -79
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -86,15 +86,133 @@ var init_bun_check = __esm(() => {
86
86
  init_spawn();
87
87
  });
88
88
 
89
+ // src/baseline.ts
90
+ var exports_baseline = {};
91
+ __export(exports_baseline, {
92
+ updateBaseline: () => updateBaseline,
93
+ saveBaseline: () => saveBaseline,
94
+ loadBaseline: () => loadBaseline,
95
+ createFindingFingerprint: () => createFindingFingerprint,
96
+ compareFindings: () => compareFindings,
97
+ calculateBaselineMetrics: () => calculateBaselineMetrics
98
+ });
99
+ import { promises as fs4 } from "node:fs";
100
+ import { createHash } from "node:crypto";
101
+ function createFindingFingerprint(finding, packageName) {
102
+ const normalizedDetails = finding.details.map((d) => d.trim().toLowerCase()).sort().join("|");
103
+ const detailsHash = createHash("md5").update(normalizedDetails).digest("hex");
104
+ return {
105
+ id: finding.id,
106
+ packageName: packageName || "root",
107
+ severity: finding.severity,
108
+ detailsHash
109
+ };
110
+ }
111
+ function calculateBaselineMetrics(findings, packages) {
112
+ const greenCount = findings.filter((f) => f.severity === "green").length;
113
+ const yellowCount = findings.filter((f) => f.severity === "yellow").length;
114
+ const redCount = findings.filter((f) => f.severity === "red").length;
115
+ const packagesGreen = packages.filter((p) => p.severity === "green").length;
116
+ const packagesYellow = packages.filter((p) => p.severity === "yellow").length;
117
+ const packagesRed = packages.filter((p) => p.severity === "red").length;
118
+ return {
119
+ totalFindings: findings.length,
120
+ greenCount,
121
+ yellowCount,
122
+ redCount,
123
+ packagesGreen,
124
+ packagesYellow,
125
+ packagesRed
126
+ };
127
+ }
128
+ function compareFindings(baseline, current) {
129
+ const baselineMap = new Map;
130
+ const currentMap = new Map;
131
+ for (const fp of baseline) {
132
+ const key = `${fp.id}:${fp.packageName}:${fp.detailsHash}`;
133
+ baselineMap.set(key, fp);
134
+ }
135
+ for (const fp of current) {
136
+ const key = `${fp.id}:${fp.packageName}:${fp.detailsHash}`;
137
+ currentMap.set(key, fp);
138
+ }
139
+ const newFindings = [];
140
+ for (const [key, fp] of currentMap.entries()) {
141
+ if (!baselineMap.has(key)) {
142
+ newFindings.push(fp);
143
+ }
144
+ }
145
+ const resolvedFindings = [];
146
+ for (const [key, fp] of baselineMap.entries()) {
147
+ if (!currentMap.has(key)) {
148
+ resolvedFindings.push(fp);
149
+ }
150
+ }
151
+ const severityChanges = [];
152
+ for (const [key, currentFp] of currentMap.entries()) {
153
+ const baselineFp = baselineMap.get(key);
154
+ if (baselineFp && currentFp.severity !== baselineFp.severity) {
155
+ severityChanges.push({
156
+ fingerprint: currentFp,
157
+ oldSeverity: baselineFp.severity,
158
+ newSeverity: currentFp.severity
159
+ });
160
+ }
161
+ }
162
+ const regressionReasons = [];
163
+ let isRegression = false;
164
+ const newRedFindings = newFindings.filter((f) => f.severity === "red");
165
+ if (newRedFindings.length > 0) {
166
+ isRegression = true;
167
+ regressionReasons.push(`New RED findings detected: ${newRedFindings.map((f) => f.id).join(", ")}`);
168
+ }
169
+ const upgradedToRed = severityChanges.filter((c) => c.newSeverity === "red");
170
+ if (upgradedToRed.length > 0) {
171
+ isRegression = true;
172
+ regressionReasons.push(`Severity upgraded to RED: ${upgradedToRed.map((c) => c.fingerprint.id).join(", ")}`);
173
+ }
174
+ return {
175
+ newFindings,
176
+ resolvedFindings,
177
+ severityChanges,
178
+ isRegression,
179
+ regressionReasons
180
+ };
181
+ }
182
+ async function saveBaseline(baseline, filePath) {
183
+ const json = JSON.stringify(baseline, null, 2);
184
+ await fs4.writeFile(filePath, json, "utf-8");
185
+ }
186
+ async function loadBaseline(filePath) {
187
+ try {
188
+ const json = await fs4.readFile(filePath, "utf-8");
189
+ const data = JSON.parse(json);
190
+ if (typeof data === "object" && data !== null && typeof data.version === "string" && typeof data.timestamp === "string" && Array.isArray(data.findings)) {
191
+ return data;
192
+ }
193
+ return null;
194
+ } catch (error) {
195
+ return null;
196
+ }
197
+ }
198
+ function updateBaseline(existing, current) {
199
+ return {
200
+ ...existing,
201
+ timestamp: new Date().toISOString(),
202
+ findings: current
203
+ };
204
+ }
205
+ var init_baseline = () => {};
206
+
89
207
  // src/cli.ts
90
- import { promises as fs3 } from "node:fs";
91
- import path5 from "node:path";
208
+ import { promises as fs5 } from "node:fs";
209
+ import path7 from "node:path";
92
210
 
93
211
  // src/analyze.ts
94
212
  init_spawn();
95
- import path4 from "node:path";
213
+ import path5 from "node:path";
96
214
  import os from "node:os";
97
- import { promises as fs2 } from "node:fs";
215
+ import { promises as fs3 } from "node:fs";
98
216
 
99
217
  // src/util.ts
100
218
  import { promises as fs } from "node:fs";
@@ -488,6 +606,173 @@ var summarizeSeverity = (findings, installOk, testOk) => {
488
606
  sev = "red";
489
607
  return sev;
490
608
  };
609
+ var calculatePackageStats = (pkg, findings) => {
610
+ const dependencies = pkg.dependencies || {};
611
+ const devDependencies = pkg.devDependencies || {};
612
+ const riskyPackageNames = new Set;
613
+ for (const finding of findings) {
614
+ for (const detail of finding.details) {
615
+ const match = detail.match(/^([a-zA-Z0-9_@\/\.\-]+)/);
616
+ if (match && match[1]) {
617
+ const fullPkg = match[1];
618
+ const pkgName = fullPkg.split(/[@:]/)[0];
619
+ if (pkgName) {
620
+ riskyPackageNames.add(pkgName);
621
+ }
622
+ }
623
+ }
624
+ }
625
+ let cleanDependencies = 0;
626
+ let riskyDependencies = 0;
627
+ let cleanDevDependencies = 0;
628
+ let riskyDevDependencies = 0;
629
+ for (const depName of Object.keys(dependencies)) {
630
+ if (riskyPackageNames.has(depName)) {
631
+ riskyDependencies++;
632
+ } else {
633
+ cleanDependencies++;
634
+ }
635
+ }
636
+ for (const depName of Object.keys(devDependencies)) {
637
+ if (riskyPackageNames.has(depName)) {
638
+ riskyDevDependencies++;
639
+ } else {
640
+ cleanDevDependencies++;
641
+ }
642
+ }
643
+ return {
644
+ totalDependencies: Object.keys(dependencies).length,
645
+ totalDevDependencies: Object.keys(devDependencies).length,
646
+ cleanDependencies,
647
+ cleanDevDependencies,
648
+ riskyDependencies,
649
+ riskyDevDependencies
650
+ };
651
+ };
652
+ var calculateFindingsSummary = (findings) => {
653
+ let green = 0;
654
+ let yellow = 0;
655
+ let red = 0;
656
+ for (const finding of findings) {
657
+ if (finding.severity === "green") {
658
+ green++;
659
+ } else if (finding.severity === "yellow") {
660
+ yellow++;
661
+ } else if (finding.severity === "red") {
662
+ red++;
663
+ }
664
+ }
665
+ return {
666
+ green,
667
+ yellow,
668
+ red,
669
+ total: green + yellow + red
670
+ };
671
+ };
672
+
673
+ // src/usage_analyzer.ts
674
+ import { promises as fs2 } from "node:fs";
675
+ import path2 from "node:path";
676
+ var SUPPORTED_EXTENSIONS = [".ts", ".js", ".tsx", ".jsx", ".mts", ".mjs"];
677
+ var IMPORT_PATTERNS = [
678
+ /import\s+(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+['"]([^./][^'"]*)['"]/g,
679
+ /import\s*\(\s*['"]([^./][^'"]*)['"]\s*\)/g,
680
+ /require\s*\(\s*['"]([^./][^'"]*)['"]\s*\)/g
681
+ ];
682
+ function extractPackageNames(content) {
683
+ const packageSet = new Set;
684
+ for (const pattern of IMPORT_PATTERNS) {
685
+ let match;
686
+ while ((match = pattern.exec(content)) !== null) {
687
+ const packageName = match[1];
688
+ if (packageName) {
689
+ packageSet.add(packageName);
690
+ }
691
+ }
692
+ }
693
+ return Array.from(packageSet);
694
+ }
695
+ async function findSourceFiles(dirPath) {
696
+ const files = [];
697
+ let entries;
698
+ try {
699
+ entries = await fs2.readdir(dirPath, { withFileTypes: true });
700
+ } catch (error) {
701
+ return files;
702
+ }
703
+ for (const entry of entries) {
704
+ const fullPath = path2.join(dirPath, entry.name);
705
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) {
706
+ continue;
707
+ }
708
+ if (entry.isDirectory()) {
709
+ const subFiles = await findSourceFiles(fullPath);
710
+ files.push(...subFiles);
711
+ } else if (entry.isFile()) {
712
+ const ext = path2.extname(entry.name);
713
+ if (SUPPORTED_EXTENSIONS.includes(ext)) {
714
+ files.push(fullPath);
715
+ }
716
+ }
717
+ }
718
+ return files;
719
+ }
720
+ function getAllPackageNames(pkg) {
721
+ const packageNames = new Set;
722
+ if (pkg.dependencies) {
723
+ Object.keys(pkg.dependencies).forEach((name) => packageNames.add(name));
724
+ }
725
+ if (pkg.devDependencies) {
726
+ Object.keys(pkg.devDependencies).forEach((name) => packageNames.add(name));
727
+ }
728
+ if (pkg.optionalDependencies) {
729
+ Object.keys(pkg.optionalDependencies).forEach((name) => packageNames.add(name));
730
+ }
731
+ return packageNames;
732
+ }
733
+ var analyzePackageUsageAsync = async (pkg, packagePath, includeDetails = true) => {
734
+ const packageNames = getAllPackageNames(pkg);
735
+ const totalPackages = packageNames.size;
736
+ const sourceFiles = await findSourceFiles(packagePath);
737
+ const analyzedFiles = sourceFiles.length;
738
+ const usageByPackage = new Map;
739
+ for (const pkgName of packageNames) {
740
+ usageByPackage.set(pkgName, {
741
+ packageName: pkgName,
742
+ fileCount: 0,
743
+ filePaths: []
744
+ });
745
+ }
746
+ for (const filePath of sourceFiles) {
747
+ try {
748
+ const content = await fs2.readFile(filePath, "utf-8");
749
+ const importedPackages = extractPackageNames(content);
750
+ for (const importedPkg of importedPackages) {
751
+ const usage = usageByPackage.get(importedPkg);
752
+ if (usage) {
753
+ usage.fileCount++;
754
+ if (includeDetails) {
755
+ const relativePath = path2.relative(packagePath, filePath);
756
+ usage.filePaths.push(relativePath);
757
+ }
758
+ usageByPackage.set(importedPkg, usage);
759
+ }
760
+ }
761
+ } catch (error) {
762
+ continue;
763
+ }
764
+ }
765
+ if (includeDetails) {
766
+ for (const usage of usageByPackage.values()) {
767
+ usage.filePaths.sort();
768
+ }
769
+ }
770
+ return {
771
+ totalPackages,
772
+ analyzedFiles,
773
+ usageByPackage
774
+ };
775
+ };
491
776
 
492
777
  // src/bun_logs.ts
493
778
  function parseInstallLogs(logs) {
@@ -543,11 +828,11 @@ function parseInstallLogs(logs) {
543
828
  }
544
829
 
545
830
  // src/workspaces.ts
546
- import path2 from "node:path";
831
+ import path3 from "node:path";
547
832
  import fsSync from "node:fs";
548
- function globMatch(pattern, path3) {
833
+ function globMatch(pattern, path4) {
549
834
  const patternParts = pattern.split("/");
550
- const pathParts = path3.split("/");
835
+ const pathParts = path4.split("/");
551
836
  let patternIdx = 0;
552
837
  let pathIdx = 0;
553
838
  while (patternIdx < patternParts.length && pathIdx < pathParts.length) {
@@ -585,16 +870,16 @@ function discoverFromWorkspaces(rootPath, workspaces) {
585
870
  const patterns = Array.isArray(workspaces) ? workspaces : workspaces.packages;
586
871
  const packages = [];
587
872
  for (const pattern of patterns) {
588
- const patternPath = path2.resolve(rootPath, pattern);
873
+ const patternPath = path3.resolve(rootPath, pattern);
589
874
  if (pattern.includes("/*") && !pattern.includes("/**")) {
590
875
  try {
591
- const baseDir = path2.dirname(patternPath);
592
- const patternName = path2.basename(patternPath);
876
+ const baseDir = path3.dirname(patternPath);
877
+ const patternName = path3.basename(patternPath);
593
878
  const entries = fsSync.readdirSync(baseDir, { withFileTypes: true });
594
879
  for (const entry of entries) {
595
880
  if (entry.isDirectory() && globMatch(patternName, entry.name)) {
596
- const packagePath = path2.join(baseDir, entry.name);
597
- const pkgJsonPath = path2.join(packagePath, "package.json");
881
+ const packagePath = path3.join(baseDir, entry.name);
882
+ const pkgJsonPath = path3.join(packagePath, "package.json");
598
883
  if (fileExistsSync(pkgJsonPath)) {
599
884
  packages.push(packagePath);
600
885
  }
@@ -615,7 +900,7 @@ function fileExistsSync(filePath) {
615
900
  }
616
901
  async function discoverWorkspaces(rootPath) {
617
902
  const packages = [];
618
- const rootPkgJson = path2.join(rootPath, "package.json");
903
+ const rootPkgJson = path3.join(rootPath, "package.json");
619
904
  if (!await fileExists(rootPkgJson)) {
620
905
  return packages;
621
906
  }
@@ -629,7 +914,7 @@ async function discoverWorkspaces(rootPath) {
629
914
  }
630
915
  const packagePaths = discoverFromWorkspaces(rootPath, workspaces);
631
916
  for (const pkgPath of packagePaths) {
632
- const pkgJsonPath = path2.join(pkgPath, "package.json");
917
+ const pkgJsonPath = path3.join(pkgPath, "package.json");
633
918
  try {
634
919
  const pkg = await readJsonFile(pkgJsonPath);
635
920
  if (pkg.name) {
@@ -647,7 +932,7 @@ async function discoverWorkspaces(rootPath) {
647
932
  return packages;
648
933
  }
649
934
  async function hasWorkspaces(rootPath) {
650
- const rootPkgJson = path2.join(rootPath, "package.json");
935
+ const rootPkgJson = path3.join(rootPath, "package.json");
651
936
  if (!await fileExists(rootPkgJson)) {
652
937
  return false;
653
938
  }
@@ -656,10 +941,10 @@ async function hasWorkspaces(rootPath) {
656
941
  }
657
942
 
658
943
  // src/config.ts
659
- import path3 from "node:path";
944
+ import path4 from "node:path";
660
945
  var CONFIG_FILE_NAME = "bun-ready.config.json";
661
946
  async function readConfig(rootPath) {
662
- const configPath = path3.join(rootPath, CONFIG_FILE_NAME);
947
+ const configPath = path4.join(rootPath, CONFIG_FILE_NAME);
663
948
  if (!await fileExists(configPath)) {
664
949
  return null;
665
950
  }
@@ -703,13 +988,16 @@ function validateConfig(config) {
703
988
  result.failOn = cfg.failOn;
704
989
  }
705
990
  }
991
+ if (typeof cfg.detailed === "boolean") {
992
+ result.detailed = cfg.detailed;
993
+ }
706
994
  if (Object.keys(result).length === 0) {
707
995
  return null;
708
996
  }
709
997
  return result;
710
998
  }
711
999
  function mergeConfigWithOpts(config, opts) {
712
- if (!config && !opts.failOn) {
1000
+ if (!config && !opts.failOn && opts.detailed === undefined) {
713
1001
  return null;
714
1002
  }
715
1003
  const result = {
@@ -718,29 +1006,32 @@ function mergeConfigWithOpts(config, opts) {
718
1006
  if (opts.failOn) {
719
1007
  result.failOn = opts.failOn;
720
1008
  }
1009
+ if (opts.detailed !== undefined) {
1010
+ result.detailed = opts.detailed;
1011
+ }
721
1012
  return Object.keys(result).length > 0 ? result : null;
722
1013
  }
723
1014
 
724
1015
  // src/analyze.ts
725
1016
  async function readRepoInfo(packagePath) {
726
- const packageJsonPath = path4.join(packagePath, "package.json");
1017
+ const packageJsonPath = path5.join(packagePath, "package.json");
727
1018
  const pkg = await readJsonFile(packageJsonPath);
728
1019
  const scripts = pkg.scripts ?? {};
729
1020
  const dependencies = pkg.dependencies ?? {};
730
1021
  const devDependencies = pkg.devDependencies ?? {};
731
1022
  const optionalDependencies = pkg.optionalDependencies ?? {};
732
1023
  const lockfiles = {
733
- bunLock: await fileExists(path4.join(packagePath, "bun.lock")),
734
- bunLockb: await fileExists(path4.join(packagePath, "bun.lockb")),
735
- npmLock: await fileExists(path4.join(packagePath, "package-lock.json")),
736
- yarnLock: await fileExists(path4.join(packagePath, "yarn.lock")),
737
- pnpmLock: await fileExists(path4.join(packagePath, "pnpm-lock.yaml"))
1024
+ bunLock: await fileExists(path5.join(packagePath, "bun.lock")),
1025
+ bunLockb: await fileExists(path5.join(packagePath, "bun.lockb")),
1026
+ npmLock: await fileExists(path5.join(packagePath, "package-lock.json")),
1027
+ yarnLock: await fileExists(path5.join(packagePath, "yarn.lock")),
1028
+ pnpmLock: await fileExists(path5.join(packagePath, "pnpm-lock.yaml"))
738
1029
  };
739
1030
  return { pkg, scripts, dependencies, devDependencies, optionalDependencies, lockfiles };
740
1031
  }
741
1032
  async function copyIfExists(from, to) {
742
1033
  try {
743
- await fs2.copyFile(from, to);
1034
+ await fs3.copyFile(from, to);
744
1035
  } catch {
745
1036
  return;
746
1037
  }
@@ -758,21 +1049,21 @@ async function runBunInstallDryRun(packagePath) {
758
1049
  skipReason
759
1050
  };
760
1051
  }
761
- const base = await fs2.mkdtemp(path4.join(os.tmpdir(), "bun-ready-"));
1052
+ const base = await fs3.mkdtemp(path5.join(os.tmpdir(), "bun-ready-"));
762
1053
  const cleanup = async () => {
763
1054
  try {
764
- await fs2.rm(base, { recursive: true, force: true });
1055
+ await fs3.rm(base, { recursive: true, force: true });
765
1056
  } catch {
766
1057
  return;
767
1058
  }
768
1059
  };
769
1060
  try {
770
- await copyIfExists(path4.join(packagePath, "package.json"), path4.join(base, "package.json"));
771
- await copyIfExists(path4.join(packagePath, "bun.lock"), path4.join(base, "bun.lock"));
772
- await copyIfExists(path4.join(packagePath, "bun.lockb"), path4.join(base, "bun.lockb"));
773
- await copyIfExists(path4.join(packagePath, "package-lock.json"), path4.join(base, "package-lock.json"));
774
- await copyIfExists(path4.join(packagePath, "yarn.lock"), path4.join(base, "yarn.lock"));
775
- await copyIfExists(path4.join(packagePath, "pnpm-lock.yaml"), path4.join(base, "pnpm-lock.yaml"));
1061
+ await copyIfExists(path5.join(packagePath, "package.json"), path5.join(base, "package.json"));
1062
+ await copyIfExists(path5.join(packagePath, "bun.lock"), path5.join(base, "bun.lock"));
1063
+ await copyIfExists(path5.join(packagePath, "bun.lockb"), path5.join(base, "bun.lockb"));
1064
+ await copyIfExists(path5.join(packagePath, "package-lock.json"), path5.join(base, "package-lock.json"));
1065
+ await copyIfExists(path5.join(packagePath, "yarn.lock"), path5.join(base, "yarn.lock"));
1066
+ await copyIfExists(path5.join(packagePath, "pnpm-lock.yaml"), path5.join(base, "pnpm-lock.yaml"));
776
1067
  const res = await exec("bun", ["install", "--dry-run"], base);
777
1068
  const combined = [...res.stdout ? res.stdout.split(`
778
1069
  `) : [], ...res.stderr ? res.stderr.split(`
@@ -818,7 +1109,7 @@ function filterFindings(findings, config) {
818
1109
  }
819
1110
  async function analyzeSinglePackage(packagePath, opts, config, pkgName) {
820
1111
  const info = await readRepoInfo(packagePath);
821
- const name = pkgName || info.pkg.name || path4.basename(packagePath);
1112
+ const name = pkgName || info.pkg.name || path5.basename(packagePath);
822
1113
  let findings = [
823
1114
  ...detectLockfileSignals({ packageJsonPath: packagePath, lockfiles: info.lockfiles, scripts: info.scripts, dependencies: info.dependencies, devDependencies: info.devDependencies, optionalDependencies: info.optionalDependencies, hasWorkspaces: false, packageJson: info.pkg }),
824
1115
  ...detectScriptRisks({ packageJsonPath: packagePath, lockfiles: info.lockfiles, scripts: info.scripts, dependencies: info.dependencies, devDependencies: info.devDependencies, optionalDependencies: info.optionalDependencies, hasWorkspaces: false, packageJson: info.pkg }),
@@ -877,13 +1168,22 @@ async function analyzeSinglePackage(packagePath, opts, config, pkgName) {
877
1168
  testOk = testResult.ok;
878
1169
  }
879
1170
  const severity = summarizeSeverity(findings, installOk, testOk);
1171
+ const stats = calculatePackageStats(info.pkg, findings);
1172
+ const findingsSummary = calculateFindingsSummary(findings);
1173
+ let packageUsage;
1174
+ if (opts.detailed) {
1175
+ try {
1176
+ const usage = await analyzePackageUsageAsync(info.pkg, packagePath, true);
1177
+ packageUsage = usage;
1178
+ } catch (error) {}
1179
+ }
880
1180
  const summaryLines = [];
881
1181
  summaryLines.push(`Lockfiles: ${info.lockfiles.bunLock || info.lockfiles.bunLockb ? "bun" : "non-bun or missing"}`);
882
1182
  summaryLines.push(`Lifecycle scripts: ${Object.keys(info.scripts).some((k) => ["postinstall", "prepare", "preinstall", "install"].includes(k)) ? "present" : "none"}`);
883
1183
  summaryLines.push(`Native addon risk: ${findings.some((f) => f.id === "deps.native_addons") ? "yes" : "no"}`);
884
1184
  summaryLines.push(`bun install dry-run: ${install ? install.ok ? "ok" : "failed" : "skipped"}`);
885
1185
  summaryLines.push(`bun test: ${test ? test.ok ? "ok" : "failed" : "skipped"}`);
886
- return {
1186
+ const result = {
887
1187
  name,
888
1188
  path: packagePath,
889
1189
  severity,
@@ -895,8 +1195,14 @@ async function analyzeSinglePackage(packagePath, opts, config, pkgName) {
895
1195
  dependencies: info.dependencies,
896
1196
  devDependencies: info.devDependencies,
897
1197
  optionalDependencies: info.optionalDependencies,
898
- lockfiles: info.lockfiles
1198
+ lockfiles: info.lockfiles,
1199
+ stats,
1200
+ findingsSummary
899
1201
  };
1202
+ if (packageUsage !== undefined) {
1203
+ result.packageUsage = packageUsage;
1204
+ }
1205
+ return result;
900
1206
  }
901
1207
  function aggregateSeverity(packages, overallSeverity) {
902
1208
  if (overallSeverity === "red")
@@ -915,7 +1221,7 @@ function aggregateSeverity(packages, overallSeverity) {
915
1221
  }
916
1222
  async function analyzeRepoOverall(opts) {
917
1223
  const repoPath = normalizeRepoPath(opts.repoPath);
918
- const packageJsonPath = path4.join(repoPath, "package.json");
1224
+ const packageJsonPath = path5.join(repoPath, "package.json");
919
1225
  const hasPkg = await fileExists(packageJsonPath);
920
1226
  const config = await readConfig(repoPath);
921
1227
  if (!hasPkg) {
@@ -1029,6 +1335,27 @@ var badge = (s) => {
1029
1335
  return "\uD83D\uDFE1 YELLOW";
1030
1336
  return "\uD83D\uDD34 RED";
1031
1337
  };
1338
+ var getReadinessMessage = (severity, hasRedFindings) => {
1339
+ if (severity === "green" && !hasRedFindings) {
1340
+ return "✅ Вітаю, ви готові до переходу на Bun!";
1341
+ }
1342
+ if (severity === "yellow") {
1343
+ return "⚠️ Нажаль ви не готові до переходу на Bun, але це можливо з деякими змінами";
1344
+ }
1345
+ return "❌ Нажаль ви не готові до переходу на Bun через критичні проблеми";
1346
+ };
1347
+ var formatFindingsTable = (summary) => {
1348
+ const lines = [];
1349
+ lines.push(`## Findings Summary`);
1350
+ lines.push(`| Status | Count |`);
1351
+ lines.push(`|--------|-------|`);
1352
+ lines.push(`| \uD83D\uDFE2 Green | ${summary.green} |`);
1353
+ lines.push(`| \uD83D\uDFE1 Yellow | ${summary.yellow} |`);
1354
+ lines.push(`| \uD83D\uDD34 Red | ${summary.red} |`);
1355
+ lines.push(`| **Total** | **${summary.total}** |`);
1356
+ return lines.join(`
1357
+ `);
1358
+ };
1032
1359
  var getTopFindings = (pkg, count = 3) => {
1033
1360
  const sorted = [...pkg.findings].sort((a, b) => {
1034
1361
  const severityOrder = { red: 0, yellow: 1, green: 2 };
@@ -1041,14 +1368,157 @@ var getTopFindings = (pkg, count = 3) => {
1041
1368
  };
1042
1369
  var packageRow = (pkg) => {
1043
1370
  const name = pkg.name;
1044
- const path5 = pkg.path.replace(/\\/g, "/");
1371
+ const path6 = pkg.path.replace(/\\/g, "/");
1045
1372
  const severity = badge(pkg.severity);
1046
1373
  const topFindings = getTopFindings(pkg, 2).join(", ") || "No issues";
1047
- return `| ${name} | \`${path5}\` | ${severity} | ${topFindings} |`;
1374
+ return `| ${name} | \`${path6}\` | ${severity} | ${topFindings} |`;
1375
+ };
1376
+ var formatPackageStats = (pkg) => {
1377
+ const lines = [];
1378
+ if (pkg.stats) {
1379
+ lines.push(`- Total dependencies: ${pkg.stats.totalDependencies}`);
1380
+ lines.push(`- Total devDependencies: ${pkg.stats.totalDevDependencies}`);
1381
+ lines.push(`- Clean dependencies: ${pkg.stats.cleanDependencies}`);
1382
+ lines.push(`- Clean devDependencies: ${pkg.stats.cleanDevDependencies}`);
1383
+ lines.push(`- Dependencies with findings: ${pkg.stats.riskyDependencies}`);
1384
+ lines.push(`- DevDependencies with findings: ${pkg.stats.riskyDevDependencies}`);
1385
+ }
1386
+ if (pkg.packageUsage) {
1387
+ lines.push(`- **Total files analyzed**: ${pkg.packageUsage.analyzedFiles}`);
1388
+ const usedPackages = Array.from(pkg.packageUsage.usageByPackage.values()).filter((u) => u.fileCount > 0).length;
1389
+ lines.push(`- **Packages used in code**: ${usedPackages}`);
1390
+ }
1391
+ return lines;
1392
+ };
1393
+ var formatPolicyApplied = (policy) => {
1394
+ const lines = [];
1395
+ lines.push(`## Policy Applied`);
1396
+ lines.push(``);
1397
+ lines.push(`- **Rules applied**: ${policy.rulesApplied}`);
1398
+ lines.push(`- **Findings modified**: ${policy.findingsModified}`);
1399
+ lines.push(`- **Findings disabled**: ${policy.findingsDisabled}`);
1400
+ lines.push(`- **Severity upgraded**: ${policy.severityUpgraded}`);
1401
+ lines.push(`- **Severity downgraded**: ${policy.severityDowngraded}`);
1402
+ lines.push(``);
1403
+ if (policy.rules.length > 0) {
1404
+ lines.push(`### Policy Rules`);
1405
+ for (const rule of policy.rules) {
1406
+ const actionBadge = getPolicyActionBadge(rule.action);
1407
+ lines.push(`- **${rule.findingId}**: ${actionBadge}`);
1408
+ if (rule.originalSeverity && rule.newSeverity && rule.originalSeverity !== rule.newSeverity) {
1409
+ lines.push(` - ${badge(rule.originalSeverity)} → ${badge(rule.newSeverity)}`);
1410
+ }
1411
+ if (rule.reason) {
1412
+ lines.push(` - Reason: ${rule.reason}`);
1413
+ }
1414
+ }
1415
+ lines.push(``);
1416
+ } else {
1417
+ lines.push(`No policy rules were applied.`);
1418
+ lines.push(``);
1419
+ }
1420
+ return lines.join(`
1421
+ `);
1422
+ };
1423
+ var getPolicyActionBadge = (action) => {
1424
+ if (action === "fail")
1425
+ return "\uD83D\uDED1 FAIL";
1426
+ if (action === "warn")
1427
+ return "⚠️ WARN";
1428
+ if (action === "off")
1429
+ return "\uD83D\uDD34 OFF";
1430
+ return "\uD83D\uDD35 IGNORE";
1431
+ };
1432
+ var formatBaselineComparison = (comparison) => {
1433
+ const lines = [];
1434
+ lines.push(`## Baseline Comparison`);
1435
+ lines.push(``);
1436
+ if (comparison.isRegression) {
1437
+ lines.push(`\uD83D\uDD34 **REGRESSION DETECTED**`);
1438
+ } else {
1439
+ lines.push(`✅ No regression detected`);
1440
+ }
1441
+ lines.push(``);
1442
+ if (comparison.newFindings.length > 0) {
1443
+ lines.push(`### New Findings (${comparison.newFindings.length})`);
1444
+ for (const f of comparison.newFindings) {
1445
+ const severityBadge = badge(f.severity);
1446
+ lines.push(`- ${severityBadge} **${f.id}** in package \`${f.packageName}\``);
1447
+ }
1448
+ lines.push(``);
1449
+ }
1450
+ if (comparison.resolvedFindings.length > 0) {
1451
+ lines.push(`### Resolved Findings (${comparison.resolvedFindings.length})`);
1452
+ for (const f of comparison.resolvedFindings) {
1453
+ const severityBadge = badge(f.severity);
1454
+ lines.push(`- ${severityBadge} **${f.id}** in package \`${f.packageName}\``);
1455
+ }
1456
+ lines.push(``);
1457
+ }
1458
+ if (comparison.severityChanges.length > 0) {
1459
+ lines.push(`### Severity Changes (${comparison.severityChanges.length})`);
1460
+ for (const c of comparison.severityChanges) {
1461
+ const oldBadge = badge(c.oldSeverity);
1462
+ const newBadge = badge(c.newSeverity);
1463
+ lines.push(`- **${c.fingerprint.id}** in package \`${c.fingerprint.packageName}\`: ${oldBadge} → ${newBadge}`);
1464
+ }
1465
+ lines.push(``);
1466
+ }
1467
+ if (comparison.regressionReasons.length > 0) {
1468
+ lines.push(`### Regression Reasons`);
1469
+ for (const reason of comparison.regressionReasons) {
1470
+ lines.push(`- ${reason}`);
1471
+ }
1472
+ lines.push(``);
1473
+ }
1474
+ if (comparison.newFindings.length === 0 && comparison.resolvedFindings.length === 0 && comparison.severityChanges.length === 0) {
1475
+ lines.push(`No changes detected from baseline.`);
1476
+ lines.push(``);
1477
+ }
1478
+ return lines.join(`
1479
+ `);
1480
+ };
1481
+ var formatChangedOnly = (changedPackages, baselineFile) => {
1482
+ const lines = [];
1483
+ lines.push(`## Scanned Packages (Changed-only)`);
1484
+ lines.push(``);
1485
+ if (changedPackages.length === 0) {
1486
+ lines.push(`No packages were identified as changed.`);
1487
+ } else {
1488
+ lines.push(`The following packages were scanned (only changed packages):`);
1489
+ lines.push(``);
1490
+ for (const pkgPath of changedPackages) {
1491
+ const normalizedPath = pkgPath.replace(/\\/g, "/");
1492
+ lines.push(`- \`${normalizedPath}\``);
1493
+ }
1494
+ }
1495
+ lines.push(``);
1496
+ if (baselineFile) {
1497
+ lines.push(`**Verdict type:** Regression verdict (comparing changed packages against baseline)`);
1498
+ } else {
1499
+ lines.push(`**Verdict type:** Partial verdict (only changed packages scanned, no baseline)`);
1500
+ lines.push(`⚠️ Note: This is a partial scan. For complete verdict, provide a baseline file.`);
1501
+ }
1502
+ lines.push(``);
1503
+ return lines.join(`
1504
+ `);
1048
1505
  };
1049
1506
  function renderMarkdown(r) {
1050
1507
  const lines = [];
1051
- lines.push(`# bun-ready report`);
1508
+ const bunVersion = process.version;
1509
+ const hasRedFindings = r.findings.some((f) => f.severity === "red");
1510
+ const readinessMessage = getReadinessMessage(r.severity, hasRedFindings);
1511
+ lines.push(`# bun-ready report - Tested with Bun ${bunVersion}`);
1512
+ lines.push(``);
1513
+ lines.push(readinessMessage);
1514
+ lines.push(``);
1515
+ const rootFindingsSummary = {
1516
+ green: r.findings.filter((f) => f.severity === "green").length,
1517
+ yellow: r.findings.filter((f) => f.severity === "yellow").length,
1518
+ red: r.findings.filter((f) => f.severity === "red").length,
1519
+ total: r.findings.length
1520
+ };
1521
+ lines.push(formatFindingsTable(rootFindingsSummary));
1052
1522
  lines.push(``);
1053
1523
  lines.push(`**Overall:** ${badge(r.severity)}`);
1054
1524
  lines.push(``);
@@ -1084,6 +1554,9 @@ function renderMarkdown(r) {
1084
1554
  }
1085
1555
  lines.push(``);
1086
1556
  }
1557
+ if (r.policyApplied) {
1558
+ lines.push(formatPolicyApplied(r.policyApplied));
1559
+ }
1087
1560
  lines.push(`## Root Package`);
1088
1561
  lines.push(`- Path: \`${r.repo.packageJsonPath.replace(/\\/g, "/")}\``);
1089
1562
  lines.push(`- Workspaces: ${r.repo.hasWorkspaces ? "yes" : "no"}`);
@@ -1127,6 +1600,13 @@ function renderMarkdown(r) {
1127
1600
  }
1128
1601
  lines.push(``);
1129
1602
  }
1603
+ const rootPkgForStats = r.packages?.find((p) => p.path === r.repo.packageJsonPath);
1604
+ if (rootPkgForStats && rootPkgForStats.stats) {
1605
+ lines.push(`## Package Summary`);
1606
+ for (const l of formatPackageStats(rootPkgForStats))
1607
+ lines.push(l);
1608
+ lines.push(``);
1609
+ }
1130
1610
  lines.push(`## Root Findings`);
1131
1611
  if (r.findings.length === 0) {
1132
1612
  lines.push(`No findings for root package.`);
@@ -1147,6 +1627,13 @@ function renderMarkdown(r) {
1147
1627
  }
1148
1628
  }
1149
1629
  lines.push(``);
1630
+ if (r.baselineComparison) {
1631
+ lines.push(formatBaselineComparison(r.baselineComparison));
1632
+ }
1633
+ if (r.changedPackages) {
1634
+ const baselineFile = r.baselineFile;
1635
+ lines.push(formatChangedOnly(r.changedPackages, baselineFile));
1636
+ }
1150
1637
  if (r.packages && r.packages.length > 0) {
1151
1638
  const sortedPackages = stableSort(r.packages, (p) => p.name);
1152
1639
  for (const pkg of sortedPackages) {
@@ -1157,6 +1644,12 @@ function renderMarkdown(r) {
1157
1644
  for (const l of pkg.summaryLines)
1158
1645
  lines.push(`- ${l}`);
1159
1646
  lines.push(``);
1647
+ if (pkg.stats) {
1648
+ lines.push(`**Package Summary**`);
1649
+ for (const l of formatPackageStats(pkg))
1650
+ lines.push(l);
1651
+ lines.push(``);
1652
+ }
1160
1653
  if (pkg.install) {
1161
1654
  lines.push(`**bun install (dry-run):** ${pkg.install.ok ? "ok" : "failed"}`);
1162
1655
  if (pkg.install.logs.length > 0 && pkg.install.logs.length < 10) {
@@ -1202,28 +1695,627 @@ function renderMarkdown(r) {
1202
1695
  return lines.join(`
1203
1696
  `);
1204
1697
  }
1698
+ var renderDetailedReport = (r) => {
1699
+ const lines = [];
1700
+ const bunVersion = process.version;
1701
+ const hasRedFindings = r.findings.some((f) => f.severity === "red");
1702
+ const readinessMessage = getReadinessMessage(r.severity, hasRedFindings);
1703
+ lines.push(`# bun-ready detailed report - Tested with Bun ${bunVersion}`);
1704
+ lines.push(``);
1705
+ lines.push(readinessMessage);
1706
+ lines.push(``);
1707
+ const rootFindingsSummary = {
1708
+ green: r.findings.filter((f) => f.severity === "green").length,
1709
+ yellow: r.findings.filter((f) => f.severity === "yellow").length,
1710
+ red: r.findings.filter((f) => f.severity === "red").length,
1711
+ total: r.findings.length
1712
+ };
1713
+ lines.push(formatFindingsTable(rootFindingsSummary));
1714
+ lines.push(``);
1715
+ lines.push(`**Overall:** ${badge(r.severity)}`);
1716
+ lines.push(``);
1717
+ if (r.policyApplied) {
1718
+ lines.push(formatPolicyApplied(r.policyApplied));
1719
+ }
1720
+ lines.push(`## Detailed Package Usage`);
1721
+ lines.push(``);
1722
+ let hasUsageInfo = false;
1723
+ if (r.packages && r.packages.length > 0) {
1724
+ const sortedPackages = stableSort(r.packages, (p) => p.name);
1725
+ for (const pkg of sortedPackages) {
1726
+ if (!pkg.packageUsage)
1727
+ continue;
1728
+ hasUsageInfo = true;
1729
+ lines.push(`### ${pkg.name}`);
1730
+ lines.push(``);
1731
+ lines.push(`**Total files analyzed:** ${pkg.packageUsage.analyzedFiles}`);
1732
+ lines.push(`**Total packages:** ${pkg.packageUsage.totalPackages}`);
1733
+ lines.push(``);
1734
+ const sortedUsage = Array.from(pkg.packageUsage.usageByPackage.values()).filter((u) => u.fileCount > 0).sort((a, b) => b.fileCount - a.fileCount);
1735
+ if (sortedUsage.length === 0) {
1736
+ lines.push(`No package usage detected in source files.`);
1737
+ lines.push(``);
1738
+ continue;
1739
+ }
1740
+ for (const usage of sortedUsage) {
1741
+ const depVersion = pkg.dependencies[usage.packageName] || pkg.devDependencies[usage.packageName] || "";
1742
+ const versionStr = depVersion ? `@${depVersion}` : "";
1743
+ lines.push(`#### ${usage.packageName}${versionStr} (${usage.fileCount} file${usage.fileCount !== 1 ? "s" : ""})`);
1744
+ lines.push(``);
1745
+ if (usage.filePaths.length > 0) {
1746
+ for (const filePath of usage.filePaths) {
1747
+ lines.push(`- ${filePath}`);
1748
+ }
1749
+ } else {
1750
+ lines.push(`- No file paths collected`);
1751
+ }
1752
+ lines.push(``);
1753
+ }
1754
+ }
1755
+ }
1756
+ if (!hasUsageInfo) {
1757
+ lines.push(`No package usage information available. Run with --detailed flag to enable usage analysis.`);
1758
+ lines.push(``);
1759
+ }
1760
+ lines.push(`---`);
1761
+ lines.push(``);
1762
+ lines.push(`## Root Findings`);
1763
+ if (r.findings.length === 0) {
1764
+ lines.push(`No findings for root package.`);
1765
+ } else {
1766
+ const findings = stableSort(r.findings, (f) => `${f.severity}:${f.id}`);
1767
+ for (const f of findings) {
1768
+ lines.push(`### ${f.title} (${badge(f.severity)})`);
1769
+ lines.push(``);
1770
+ for (const d of f.details)
1771
+ lines.push(`- ${d}`);
1772
+ if (f.hints.length > 0) {
1773
+ lines.push(``);
1774
+ lines.push(`**Hints:**`);
1775
+ for (const h of f.hints)
1776
+ lines.push(`- ${h}`);
1777
+ }
1778
+ lines.push(``);
1779
+ }
1780
+ }
1781
+ lines.push(``);
1782
+ if (r.baselineComparison) {
1783
+ lines.push(formatBaselineComparison(r.baselineComparison));
1784
+ }
1785
+ if (r.changedPackages) {
1786
+ const baselineFile = r.baselineFile;
1787
+ lines.push(formatChangedOnly(r.changedPackages, baselineFile));
1788
+ }
1789
+ return lines.join(`
1790
+ `);
1791
+ };
1205
1792
 
1206
1793
  // src/report_json.ts
1207
1794
  function renderJson(r) {
1208
1795
  return JSON.stringify(r, null, 2);
1209
1796
  }
1210
1797
 
1798
+ // src/sarif.ts
1799
+ import path6 from "node:path";
1800
+ function severityToSarifLevel(severity) {
1801
+ switch (severity) {
1802
+ case "green":
1803
+ return "note";
1804
+ case "yellow":
1805
+ return "warning";
1806
+ case "red":
1807
+ return "error";
1808
+ }
1809
+ }
1810
+ function createSarifRule(finding) {
1811
+ const descriptionText = finding.details.length > 0 ? finding.details[0] : finding.title;
1812
+ const fullDesc = { text: descriptionText };
1813
+ const helpParts = [];
1814
+ if (finding.details.length > 0) {
1815
+ helpParts.push("Details:");
1816
+ finding.details.forEach((d) => helpParts.push(` - ${d}`));
1817
+ }
1818
+ if (finding.hints.length > 0) {
1819
+ helpParts.push("Hints:");
1820
+ finding.hints.forEach((h) => helpParts.push(` - ${h}`));
1821
+ }
1822
+ const helpText = helpParts.length > 0 ? helpParts.join(`
1823
+ `) : "No hints available";
1824
+ return {
1825
+ id: finding.id,
1826
+ shortDescription: {
1827
+ text: finding.title
1828
+ },
1829
+ fullDescription: fullDesc,
1830
+ help: {
1831
+ text: helpText
1832
+ },
1833
+ defaultConfiguration: {
1834
+ level: severityToSarifLevel(finding.severity)
1835
+ }
1836
+ };
1837
+ }
1838
+ function determineFindingLocation(finding, repoPath, packageName) {
1839
+ if (!packageName) {
1840
+ return {
1841
+ physicalLocation: {
1842
+ artifactLocation: {
1843
+ uri: path6.basename(repoPath)
1844
+ }
1845
+ }
1846
+ };
1847
+ }
1848
+ const relativePath = packageName === "root" ? "package.json" : packageName;
1849
+ return {
1850
+ physicalLocation: {
1851
+ artifactLocation: {
1852
+ uri: relativePath
1853
+ }
1854
+ }
1855
+ };
1856
+ }
1857
+ function createSarifResult(finding, repoPath, packageName) {
1858
+ const messageParts = [finding.title];
1859
+ if (finding.details.length > 0) {
1860
+ messageParts.push("");
1861
+ messageParts.push("Details:");
1862
+ messageParts.push(...finding.details.map((d) => `- ${d}`));
1863
+ }
1864
+ const messageText = messageParts.join(`
1865
+ `);
1866
+ return {
1867
+ ruleId: finding.id,
1868
+ level: severityToSarifLevel(finding.severity),
1869
+ message: {
1870
+ text: messageText
1871
+ },
1872
+ locations: [determineFindingLocation(finding, repoPath, packageName)]
1873
+ };
1874
+ }
1875
+ function renderSarif(result) {
1876
+ const allFindings = [...result.findings];
1877
+ if (result.packages) {
1878
+ for (const pkg of result.packages) {
1879
+ for (const finding of pkg.findings) {
1880
+ if (!allFindings.some((f) => f.id === finding.id)) {
1881
+ allFindings.push(finding);
1882
+ }
1883
+ }
1884
+ }
1885
+ }
1886
+ const rulesMap = new Map;
1887
+ for (const finding of allFindings) {
1888
+ if (!rulesMap.has(finding.id)) {
1889
+ rulesMap.set(finding.id, createSarifRule(finding));
1890
+ }
1891
+ }
1892
+ const rules = Array.from(rulesMap.values()).sort((a, b) => a.id.localeCompare(b.id));
1893
+ const results = [];
1894
+ for (const finding of result.findings) {
1895
+ results.push(createSarifResult(finding, result.repo.packageJsonPath, "root"));
1896
+ }
1897
+ for (const pkg of result.packages || []) {
1898
+ const packageName = pkg.name;
1899
+ for (const finding of pkg.findings) {
1900
+ results.push(createSarifResult(finding, result.repo.packageJsonPath, packageName));
1901
+ }
1902
+ }
1903
+ const sarifLog = {
1904
+ version: "2.1.0",
1905
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
1906
+ runs: [
1907
+ {
1908
+ tool: {
1909
+ driver: {
1910
+ name: "bun-ready",
1911
+ version: result.version || "0.3.0",
1912
+ semanticVersion: "2.1.0",
1913
+ rules
1914
+ }
1915
+ },
1916
+ results
1917
+ }
1918
+ ]
1919
+ };
1920
+ return sarifLog;
1921
+ }
1922
+
1923
+ // src/ci_summary.ts
1924
+ function getTopFindings2(findings, count = 3) {
1925
+ const sorted = [...findings].sort((a, b) => {
1926
+ const severityOrder = { red: 0, yellow: 1, green: 2 };
1927
+ if (severityOrder[a.severity] !== severityOrder[b.severity]) {
1928
+ return severityOrder[a.severity] - severityOrder[b.severity];
1929
+ }
1930
+ return a.id.localeCompare(b.id);
1931
+ });
1932
+ const badge2 = (s) => {
1933
+ switch (s) {
1934
+ case "green":
1935
+ return "\uD83D\uDFE2";
1936
+ case "yellow":
1937
+ return "\uD83D\uDFE1";
1938
+ case "red":
1939
+ return "\uD83D\uDD34";
1940
+ }
1941
+ };
1942
+ return sorted.slice(0, count).map((f) => `${badge2(f.severity)} ${f.title} (${f.id})`);
1943
+ }
1944
+ function generateNextActions(findings) {
1945
+ const actions = [];
1946
+ const actionSet = new Set;
1947
+ for (const finding of findings) {
1948
+ for (const hint of finding.hints) {
1949
+ const action = hint.trim();
1950
+ if (!actionSet.has(action)) {
1951
+ actions.push(action);
1952
+ actionSet.add(action);
1953
+ }
1954
+ }
1955
+ }
1956
+ return actions.slice(0, 5);
1957
+ }
1958
+ function calculateExitCode(severity, failOn) {
1959
+ if (!failOn) {
1960
+ if (severity === "green")
1961
+ return 0;
1962
+ if (severity === "yellow")
1963
+ return 2;
1964
+ return 3;
1965
+ }
1966
+ if (failOn === "green") {
1967
+ if (severity === "green")
1968
+ return 0;
1969
+ return 3;
1970
+ }
1971
+ if (failOn === "yellow") {
1972
+ if (severity === "red")
1973
+ return 3;
1974
+ return 0;
1975
+ }
1976
+ if (severity === "green")
1977
+ return 0;
1978
+ if (severity === "yellow")
1979
+ return 2;
1980
+ return 3;
1981
+ }
1982
+ function generateCISummary(result, failOn) {
1983
+ const topFindings = getTopFindings2(result.findings, 3);
1984
+ const nextActions = generateNextActions(result.findings);
1985
+ const exitCode = calculateExitCode(result.severity, failOn);
1986
+ return {
1987
+ verdict: result.severity,
1988
+ topFindings,
1989
+ nextActions,
1990
+ exitCode
1991
+ };
1992
+ }
1993
+ function formatCISummaryText(summary) {
1994
+ const badge2 = (s) => {
1995
+ switch (s) {
1996
+ case "green":
1997
+ return "\uD83D\uDFE2 GREEN";
1998
+ case "yellow":
1999
+ return "\uD83D\uDFE1 YELLOW";
2000
+ case "red":
2001
+ return "\uD83D\uDD34 RED";
2002
+ }
2003
+ };
2004
+ const lines = [];
2005
+ lines.push("=== bun-ready CI Summary ===");
2006
+ lines.push("");
2007
+ lines.push(`Verdict: ${badge2(summary.verdict)}`);
2008
+ lines.push("");
2009
+ if (summary.topFindings.length > 0) {
2010
+ lines.push("Top Issues:");
2011
+ for (const finding of summary.topFindings) {
2012
+ lines.push(` - ${finding}`);
2013
+ }
2014
+ lines.push("");
2015
+ }
2016
+ if (summary.nextActions.length > 0) {
2017
+ lines.push("Next Actions:");
2018
+ for (let i = 0;i < summary.nextActions.length; i++) {
2019
+ lines.push(` ${i + 1}. ${summary.nextActions[i]}`);
2020
+ }
2021
+ lines.push("");
2022
+ }
2023
+ lines.push(`Exit Code: ${summary.exitCode}`);
2024
+ return lines.join(`
2025
+ `);
2026
+ }
2027
+ function formatGitHubJobSummary(summary) {
2028
+ const badge2 = (s) => {
2029
+ switch (s) {
2030
+ case "green":
2031
+ return "\uD83D\uDFE2 **GREEN**";
2032
+ case "yellow":
2033
+ return "\uD83D\uDFE1 **YELLOW**";
2034
+ case "red":
2035
+ return "\uD83D\uDD34 **RED**";
2036
+ }
2037
+ };
2038
+ const lines = [];
2039
+ lines.push("## bun-ready CI Summary");
2040
+ lines.push("");
2041
+ lines.push(`### Verdict: ${badge2(summary.verdict)}`);
2042
+ lines.push("");
2043
+ if (summary.topFindings.length > 0) {
2044
+ lines.push("### Top Issues");
2045
+ lines.push("");
2046
+ for (const finding of summary.topFindings) {
2047
+ lines.push(`- ${finding}`);
2048
+ }
2049
+ lines.push("");
2050
+ }
2051
+ if (summary.nextActions.length > 0) {
2052
+ lines.push("### Next Actions");
2053
+ lines.push("");
2054
+ for (let i = 0;i < summary.nextActions.length; i++) {
2055
+ lines.push(`${i + 1}. ${summary.nextActions[i]}`);
2056
+ }
2057
+ lines.push("");
2058
+ }
2059
+ lines.push(`**Exit Code:** \`${summary.exitCode}\``);
2060
+ return lines.join(`
2061
+ `);
2062
+ }
2063
+
2064
+ // src/policy.ts
2065
+ function applySeverityChange(originalSeverity, change) {
2066
+ if (change === "same")
2067
+ return originalSeverity;
2068
+ if (change === "upgrade") {
2069
+ if (originalSeverity === "green")
2070
+ return "yellow";
2071
+ if (originalSeverity === "yellow")
2072
+ return "red";
2073
+ return "red";
2074
+ }
2075
+ if (originalSeverity === "red")
2076
+ return "yellow";
2077
+ if (originalSeverity === "yellow")
2078
+ return "green";
2079
+ return "green";
2080
+ }
2081
+ function parseRuleArgs(ruleArgs) {
2082
+ const rules = [];
2083
+ for (const arg of ruleArgs) {
2084
+ const parts = arg.split(/[=:]/, 2);
2085
+ if (parts.length !== 2)
2086
+ continue;
2087
+ const [id, actionOrChange] = parts.map((p) => p.trim());
2088
+ if (!id || !actionOrChange) {
2089
+ continue;
2090
+ }
2091
+ if (["fail", "warn", "off", "ignore"].includes(actionOrChange)) {
2092
+ const actionRule = {
2093
+ id,
2094
+ action: actionOrChange
2095
+ };
2096
+ rules.push(actionRule);
2097
+ } else if (["upgrade", "downgrade", "same"].includes(actionOrChange)) {
2098
+ const severityRule = {
2099
+ id,
2100
+ severityChange: actionOrChange
2101
+ };
2102
+ rules.push(severityRule);
2103
+ }
2104
+ }
2105
+ return rules;
2106
+ }
2107
+ function mergePolicyConfigs(cliPolicy, configPolicy) {
2108
+ if (!cliPolicy && !configPolicy) {
2109
+ return;
2110
+ }
2111
+ const result = {};
2112
+ if (cliPolicy?.rules && cliPolicy.rules.length > 0) {
2113
+ result.rules = cliPolicy.rules;
2114
+ } else if (configPolicy?.rules && configPolicy.rules.length > 0) {
2115
+ result.rules = configPolicy.rules;
2116
+ }
2117
+ if (cliPolicy?.thresholds && Object.keys(cliPolicy.thresholds).length > 0) {
2118
+ result.thresholds = cliPolicy.thresholds;
2119
+ } else if (configPolicy?.thresholds && Object.keys(configPolicy.thresholds).length > 0) {
2120
+ result.thresholds = configPolicy.thresholds;
2121
+ }
2122
+ if (cliPolicy?.failOn) {
2123
+ result.failOn = cliPolicy.failOn;
2124
+ } else if (configPolicy?.failOn) {
2125
+ result.failOn = configPolicy.failOn;
2126
+ }
2127
+ if (Object.keys(result).length === 0) {
2128
+ return;
2129
+ }
2130
+ return result;
2131
+ }
2132
+ function findMatchingRule(findingId, rules) {
2133
+ for (const rule of rules) {
2134
+ if (rule.id === findingId) {
2135
+ return rule;
2136
+ }
2137
+ }
2138
+ for (const rule of rules) {
2139
+ if (rule.id === "*") {
2140
+ return rule;
2141
+ }
2142
+ }
2143
+ return null;
2144
+ }
2145
+ function applyPolicy(findings, policy, metrics) {
2146
+ const rules = policy.rules || [];
2147
+ const thresholds = policy.thresholds;
2148
+ const modifiedFindings = [];
2149
+ const appliedRules = [];
2150
+ let findingsModified = 0;
2151
+ let findingsDisabled = 0;
2152
+ let severityUpgraded = 0;
2153
+ let severityDowngraded = 0;
2154
+ for (const finding of findings) {
2155
+ const rule = findMatchingRule(finding.id, rules);
2156
+ if (!rule) {
2157
+ modifiedFindings.push(finding);
2158
+ continue;
2159
+ }
2160
+ const applied = {
2161
+ findingId: finding.id,
2162
+ action: rule.action || "ignore",
2163
+ originalSeverity: finding.severity
2164
+ };
2165
+ if (rule.action === "off" || rule.action === "ignore") {
2166
+ findingsDisabled++;
2167
+ appliedRules.push(applied);
2168
+ continue;
2169
+ }
2170
+ let newSeverity = finding.severity;
2171
+ let wasModified = false;
2172
+ if (rule.severityChange) {
2173
+ newSeverity = finding.severity;
2174
+ if (rule.action === "fail") {
2175
+ newSeverity = "red";
2176
+ } else if (rule.action === "warn") {
2177
+ newSeverity = "yellow";
2178
+ }
2179
+ newSeverity = applySeverityChange(newSeverity, rule.severityChange);
2180
+ if (rule.severityChange === "upgrade") {
2181
+ severityUpgraded++;
2182
+ } else if (rule.severityChange === "downgrade") {
2183
+ severityDowngraded++;
2184
+ }
2185
+ wasModified = true;
2186
+ } else if (rule.action === "fail" || rule.action === "warn") {
2187
+ if (rule.action === "fail") {
2188
+ newSeverity = "red";
2189
+ } else if (rule.action === "warn") {
2190
+ newSeverity = "yellow";
2191
+ }
2192
+ wasModified = true;
2193
+ if (newSeverity !== finding.severity) {
2194
+ const severityOrder = { green: 0, yellow: 1, red: 2 };
2195
+ if (severityOrder[newSeverity] > severityOrder[finding.severity]) {
2196
+ severityUpgraded++;
2197
+ } else {
2198
+ severityDowngraded++;
2199
+ }
2200
+ }
2201
+ }
2202
+ if (wasModified) {
2203
+ findingsModified++;
2204
+ }
2205
+ applied.newSeverity = newSeverity;
2206
+ applied.reason = rule.reason;
2207
+ modifiedFindings.push({
2208
+ ...finding,
2209
+ severity: newSeverity
2210
+ });
2211
+ appliedRules.push(applied);
2212
+ }
2213
+ let rulesApplied = appliedRules.length;
2214
+ if (thresholds && metrics) {
2215
+ if (thresholds.maxPackagesRed !== undefined && metrics.packagesRed > thresholds.maxPackagesRed) {
2216
+ rulesApplied++;
2217
+ }
2218
+ if (thresholds.maxPackagesYellow !== undefined && metrics.packagesYellow > thresholds.maxPackagesYellow) {
2219
+ rulesApplied++;
2220
+ }
2221
+ }
2222
+ const summary = {
2223
+ rulesApplied,
2224
+ findingsModified,
2225
+ findingsDisabled,
2226
+ severityUpgraded,
2227
+ severityDowngraded,
2228
+ rules: appliedRules
2229
+ };
2230
+ return { modifiedFindings, summary };
2231
+ }
2232
+
2233
+ // src/cli.ts
2234
+ init_baseline();
2235
+
2236
+ // src/changed_only.ts
2237
+ init_spawn();
2238
+ async function getGitDiffPaths(repoPath, sinceRef) {
2239
+ try {
2240
+ const res = await exec("git", ["diff", "--name-only", sinceRef], repoPath);
2241
+ if (!res.stdout) {
2242
+ return [];
2243
+ }
2244
+ const paths = res.stdout.split(`
2245
+ `).map((p) => p.trim()).filter((p) => p.length > 0);
2246
+ return paths;
2247
+ } catch (error) {
2248
+ return [];
2249
+ }
2250
+ }
2251
+ function isWorkspacePackage(path7, workspacePackages) {
2252
+ if (!workspacePackages || workspacePackages.length === 0) {
2253
+ return false;
2254
+ }
2255
+ for (const wp of workspacePackages) {
2256
+ const normalizedPath = path7.replace(/\\/g, "/");
2257
+ const normalizedWpPath = wp.path.replace(/\\/g, "/");
2258
+ if (normalizedPath.startsWith(normalizedWpPath)) {
2259
+ return true;
2260
+ }
2261
+ }
2262
+ return false;
2263
+ }
2264
+ function mapPathsToPackages(paths, packages) {
2265
+ const packageMap = new Map;
2266
+ for (const pkg of packages) {
2267
+ const normalizedPath = pkg.path.replace(/\\/g, "/");
2268
+ packageMap.set(normalizedPath, pkg.name);
2269
+ }
2270
+ const changedPackages = new Set;
2271
+ for (const path7 of paths) {
2272
+ for (const [pkgPath, pkgName] of packageMap.entries()) {
2273
+ const relativePath = path7.replace(/\\/g, "/");
2274
+ if (relativePath.startsWith(pkgPath)) {
2275
+ changedPackages.add(pkgPath);
2276
+ break;
2277
+ }
2278
+ }
2279
+ }
2280
+ return Array.from(changedPackages).sort();
2281
+ }
2282
+ async function detectChangedPackages(repoPath, sinceRef, workspacePackages) {
2283
+ const paths = await getGitDiffPaths(repoPath, sinceRef);
2284
+ if (paths.length === 0) {
2285
+ return [];
2286
+ }
2287
+ let filteredPaths = paths;
2288
+ if (workspacePackages && workspacePackages.length > 0) {
2289
+ filteredPaths = paths.filter((p) => isWorkspacePackage(p, workspacePackages));
2290
+ }
2291
+ return filteredPaths;
2292
+ }
2293
+
1211
2294
  // src/cli.ts
1212
2295
  var usage = () => {
1213
2296
  return [
1214
2297
  "bun-ready",
1215
2298
  "",
1216
2299
  "Usage:",
1217
- " bun-ready scan <path> [--format md|json] [--out <file>] [--no-install] [--no-test] [--verbose] [--scope root|packages|all] [--fail-on green|yellow|red]",
2300
+ " bun-ready scan <path> [--format md|json|sarif] [--out <file>] [--no-install] [--no-test] [--verbose] [--detailed] [--scope root|packages|all] [--fail-on green|yellow|red] [--ci] [--output-dir <dir>] [--rule <id>=<action>] [--max-warnings <n>] [--baseline <file>] [--update-baseline] [--changed-only] [--since <ref>]",
1218
2301
  "",
1219
2302
  "Options:",
1220
- " --format md|json Output format (default: md)",
2303
+ " --format md|json|sarif Output format (default: md)",
1221
2304
  " --out <file> Output file path (default: bun-ready.md or bun-ready.json)",
1222
2305
  " --no-install Skip bun install --dry-run",
1223
2306
  " --no-test Skip bun test",
1224
2307
  " --verbose Show detailed output",
2308
+ " --detailed Show detailed package usage report with file paths",
1225
2309
  " --scope root|packages|all Scan scope for monorepos (default: all)",
1226
2310
  " --fail-on green|yellow|red Fail policy (default: red)",
2311
+ " --ci Run in CI mode (stable output, minimal noise)",
2312
+ " --output-dir <dir> Output directory for all artifacts (in CI mode)",
2313
+ " --rule <id>=<action> Apply policy rule (e.g., --rule deps.native_addons=fail)",
2314
+ " --max-warnings <n> Maximum warnings allowed (threshold)",
2315
+ " --baseline <file> Baseline file for regression detection",
2316
+ " --update-baseline Update baseline file after scan",
2317
+ " --changed-only Scan only changed packages (monorepos)",
2318
+ " --since <ref> Git ref for changed packages (e.g., main, HEAD~1)",
1227
2319
  "",
1228
2320
  "Exit codes:",
1229
2321
  " 0 green",
@@ -1246,6 +2338,7 @@ var parseArgs = (argv) => {
1246
2338
  runInstall: true,
1247
2339
  runTest: true,
1248
2340
  verbose: false,
2341
+ detailed: false,
1249
2342
  scope: "all"
1250
2343
  }
1251
2344
  };
@@ -1256,13 +2349,20 @@ var parseArgs = (argv) => {
1256
2349
  let runInstall = true;
1257
2350
  let runTest = true;
1258
2351
  let verbose = false;
2352
+ let detailed = false;
1259
2353
  let scope = "all";
1260
2354
  let failOn;
2355
+ let ci;
2356
+ let outputDir;
2357
+ let ruleArgs = [];
2358
+ let maxWarnings;
2359
+ let baseline;
2360
+ let changedOnly;
1261
2361
  for (let i = 2;i < args.length; i++) {
1262
2362
  const a = args[i] ?? "";
1263
2363
  if (a === "--format") {
1264
2364
  const v = args[i + 1] ?? "";
1265
- if (v === "md" || v === "json")
2365
+ if (v === "md" || v === "json" || v === "sarif")
1266
2366
  format = v;
1267
2367
  i++;
1268
2368
  continue;
@@ -1284,6 +2384,10 @@ var parseArgs = (argv) => {
1284
2384
  verbose = true;
1285
2385
  continue;
1286
2386
  }
2387
+ if (a === "--detailed") {
2388
+ detailed = true;
2389
+ continue;
2390
+ }
1287
2391
  if (a === "--scope") {
1288
2392
  const v = args[i + 1] ?? "";
1289
2393
  if (v === "root" || v === "packages" || v === "all")
@@ -1298,6 +2402,59 @@ var parseArgs = (argv) => {
1298
2402
  i++;
1299
2403
  continue;
1300
2404
  }
2405
+ if (a === "--ci") {
2406
+ ci = { mode: true };
2407
+ continue;
2408
+ }
2409
+ if (a === "--output-dir") {
2410
+ outputDir = args[i + 1];
2411
+ i++;
2412
+ continue;
2413
+ }
2414
+ if (a === "--rule") {
2415
+ ruleArgs.push(args[i + 1] ?? "");
2416
+ i++;
2417
+ continue;
2418
+ }
2419
+ if (a === "--max-warnings") {
2420
+ const v = args[i + 1] ?? "";
2421
+ const num = parseInt(v, 10);
2422
+ if (!isNaN(num) && num >= 0) {
2423
+ maxWarnings = num;
2424
+ }
2425
+ i++;
2426
+ continue;
2427
+ }
2428
+ if (a === "--baseline") {
2429
+ baseline = { file: args[i + 1] ?? "" };
2430
+ i++;
2431
+ continue;
2432
+ }
2433
+ if (a === "--update-baseline") {
2434
+ if (!baseline) {
2435
+ baseline = { file: "", update: true };
2436
+ } else {
2437
+ baseline.update = true;
2438
+ }
2439
+ continue;
2440
+ }
2441
+ if (a === "--changed-only") {
2442
+ if (!changedOnly) {
2443
+ changedOnly = { enabled: true };
2444
+ } else {
2445
+ changedOnly.enabled = true;
2446
+ }
2447
+ continue;
2448
+ }
2449
+ if (a === "--since") {
2450
+ if (!changedOnly) {
2451
+ changedOnly = { enabled: true, sinceRef: args[i + 1] ?? "" };
2452
+ } else {
2453
+ changedOnly.sinceRef = args[i + 1] ?? "";
2454
+ }
2455
+ i++;
2456
+ continue;
2457
+ }
1301
2458
  }
1302
2459
  const baseOpts = {
1303
2460
  repoPath,
@@ -1306,40 +2463,42 @@ var parseArgs = (argv) => {
1306
2463
  runInstall,
1307
2464
  runTest,
1308
2465
  verbose,
2466
+ detailed,
1309
2467
  scope
1310
2468
  };
1311
2469
  if (failOn !== undefined) {
1312
2470
  baseOpts.failOn = failOn;
1313
2471
  }
2472
+ if (ci !== undefined) {
2473
+ baseOpts.ci = ci;
2474
+ }
2475
+ if (outputDir !== undefined) {
2476
+ if (!baseOpts.ci) {
2477
+ baseOpts.ci = { mode: true };
2478
+ }
2479
+ baseOpts.ci.outputDir = outputDir;
2480
+ }
2481
+ if (ruleArgs.length > 0 || maxWarnings !== undefined) {
2482
+ const policy = {};
2483
+ if (ruleArgs.length > 0) {
2484
+ policy.rules = parseRuleArgs(ruleArgs);
2485
+ }
2486
+ if (maxWarnings !== undefined) {
2487
+ policy.thresholds = { maxWarnings };
2488
+ }
2489
+ baseOpts.policy = policy;
2490
+ }
2491
+ if (baseline !== undefined && (baseline.file || baseline.update)) {
2492
+ baseOpts.baseline = baseline;
2493
+ }
2494
+ if (changedOnly !== undefined && changedOnly.enabled) {
2495
+ baseOpts.changedOnly = changedOnly;
2496
+ }
1314
2497
  return {
1315
2498
  cmd,
1316
2499
  opts: baseOpts
1317
2500
  };
1318
2501
  };
1319
- var exitCode = (sev, failOn) => {
1320
- if (!failOn) {
1321
- if (sev === "green")
1322
- return 0;
1323
- if (sev === "yellow")
1324
- return 2;
1325
- return 3;
1326
- }
1327
- if (failOn === "green") {
1328
- if (sev === "green")
1329
- return 0;
1330
- return 3;
1331
- }
1332
- if (failOn === "yellow") {
1333
- if (sev === "red")
1334
- return 3;
1335
- return 0;
1336
- }
1337
- if (sev === "green")
1338
- return 0;
1339
- if (sev === "yellow")
1340
- return 2;
1341
- return 3;
1342
- };
1343
2502
  var main = async () => {
1344
2503
  const { cmd, opts } = parseArgs(process.argv);
1345
2504
  if (cmd !== "scan") {
@@ -1347,7 +2506,15 @@ var main = async () => {
1347
2506
  `);
1348
2507
  process.exit(1);
1349
2508
  }
1350
- const config = await mergeConfigWithOpts(null, opts);
2509
+ const configOpts = {};
2510
+ if (opts.failOn !== undefined) {
2511
+ configOpts.failOn = opts.failOn;
2512
+ }
2513
+ if (opts.detailed !== undefined) {
2514
+ configOpts.detailed = opts.detailed;
2515
+ }
2516
+ const config = await mergeConfigWithOpts(null, configOpts);
2517
+ const mergedPolicy = mergePolicyConfigs(opts.policy, config?.rules ? { rules: config.rules } : undefined);
1351
2518
  const scanOpts = {
1352
2519
  repoPath: opts.repoPath,
1353
2520
  format: opts.format,
@@ -1360,14 +2527,78 @@ var main = async () => {
1360
2527
  if (opts.failOn !== undefined) {
1361
2528
  scanOpts.failOn = opts.failOn;
1362
2529
  }
2530
+ if (opts.ci !== undefined) {
2531
+ scanOpts.ci = opts.ci;
2532
+ }
2533
+ if (mergedPolicy !== undefined) {
2534
+ scanOpts.policy = mergedPolicy;
2535
+ }
1363
2536
  const res = await analyzeRepoOverall(scanOpts);
1364
- if (res.install?.skipReason || res.test?.skipReason) {
2537
+ let changedPackages;
2538
+ if (opts.changedOnly?.enabled && opts.changedOnly?.sinceRef) {
2539
+ try {
2540
+ const detectedPaths = await detectChangedPackages(opts.repoPath, opts.changedOnly.sinceRef, res.packages?.map((p) => ({ path: p.path, packageJsonPath: p.path, name: p.name })));
2541
+ if (res.packages) {
2542
+ changedPackages = mapPathsToPackages(detectedPaths, res.packages);
2543
+ }
2544
+ } catch (error) {
2545
+ process.stderr.write(`Failed to detect changed packages: ${error instanceof Error ? error.message : String(error)}
2546
+ `);
2547
+ }
2548
+ }
2549
+ let baselineData = null;
2550
+ if (opts.baseline?.file) {
2551
+ try {
2552
+ baselineData = await loadBaseline(opts.baseline.file);
2553
+ } catch (error) {
2554
+ process.stderr.write(`Failed to load baseline: ${error instanceof Error ? error.message : String(error)}
2555
+ `);
2556
+ process.exit(1);
2557
+ }
2558
+ }
2559
+ let finalResult = res;
2560
+ if (mergedPolicy) {
2561
+ const policyResult = applyPolicy(res.findings, mergedPolicy);
2562
+ finalResult = {
2563
+ ...res,
2564
+ findings: policyResult.modifiedFindings,
2565
+ policyApplied: policyResult.summary
2566
+ };
2567
+ }
2568
+ const { createFindingFingerprint: createFindingFingerprint2, calculateBaselineMetrics: calculateBaselineMetrics2 } = await Promise.resolve().then(() => (init_baseline(), exports_baseline));
2569
+ const packages = finalResult.packages || [];
2570
+ const allFindings = [...finalResult.findings];
2571
+ for (const pkg of packages) {
2572
+ for (const finding of pkg.findings) {
2573
+ allFindings.push(finding);
2574
+ }
2575
+ }
2576
+ const currentFingerprints = allFindings.map((f) => createFindingFingerprint2(f));
2577
+ if (baselineData) {
2578
+ const comparison = compareFindings(baselineData.findings, currentFingerprints);
2579
+ finalResult = {
2580
+ ...finalResult,
2581
+ baselineComparison: comparison
2582
+ };
2583
+ if (opts.baseline?.update) {
2584
+ const updatedBaseline = {
2585
+ ...baselineData,
2586
+ timestamp: new Date().toISOString(),
2587
+ findings: currentFingerprints
2588
+ };
2589
+ await saveBaseline(updatedBaseline, opts.baseline.file || "");
2590
+ }
2591
+ }
2592
+ if (changedPackages) {
2593
+ finalResult.changedPackages = changedPackages;
2594
+ }
2595
+ if (finalResult.install?.skipReason || finalResult.test?.skipReason) {
1365
2596
  const skipWarnings = [];
1366
- if (res.install?.skipReason) {
1367
- skipWarnings.push(`Install check skipped: ${res.install.skipReason}`);
2597
+ if (finalResult.install?.skipReason) {
2598
+ skipWarnings.push(`Install check skipped: ${finalResult.install.skipReason}`);
1368
2599
  }
1369
- if (res.test?.skipReason) {
1370
- skipWarnings.push(`Test run skipped: ${res.test.skipReason}`);
2600
+ if (finalResult.test?.skipReason) {
2601
+ skipWarnings.push(`Test run skipped: ${finalResult.test.skipReason}`);
1371
2602
  }
1372
2603
  if (skipWarnings.length > 0) {
1373
2604
  process.stderr.write(`WARNING:
@@ -1376,13 +2607,48 @@ ${skipWarnings.map((w) => ` - ${w}`).join(`
1376
2607
  `);
1377
2608
  }
1378
2609
  }
1379
- const out = opts.format === "json" ? renderJson(res) : renderMarkdown(res);
1380
- const target = opts.outFile ?? (opts.format === "json" ? "bun-ready.json" : "bun-ready.md");
1381
- const resolved = path5.resolve(process.cwd(), target);
1382
- await fs3.writeFile(resolved, out, "utf8");
1383
- process.stdout.write(`Wrote ${opts.format.toUpperCase()} report to ${resolved}
2610
+ let out = "";
2611
+ if (opts.format === "sarif") {
2612
+ out = JSON.stringify(renderSarif(finalResult), null, 2);
2613
+ } else if (opts.format === "json") {
2614
+ out = renderJson(finalResult);
2615
+ } else {
2616
+ out = opts.detailed ? renderDetailedReport(finalResult) : renderMarkdown(finalResult);
2617
+ }
2618
+ let target = opts.outFile;
2619
+ let outputDir = opts.ci?.outputDir;
2620
+ if (!target) {
2621
+ if (opts.format === "sarif") {
2622
+ target = "bun-ready.sarif.json";
2623
+ } else if (opts.format === "json") {
2624
+ target = "bun-ready.json";
2625
+ } else if (opts.detailed) {
2626
+ target = "bun-ready-detailed.md";
2627
+ } else {
2628
+ target = "bun-ready.md";
2629
+ }
2630
+ }
2631
+ const resolved = outputDir ? path7.resolve(process.cwd(), outputDir, target) : path7.resolve(process.cwd(), target);
2632
+ if (outputDir) {
2633
+ await fs5.mkdir(path7.dirname(resolved), { recursive: true });
2634
+ }
2635
+ await fs5.writeFile(resolved, out, "utf8");
2636
+ if (opts.ci?.mode) {
2637
+ const ciSummary = generateCISummary(finalResult, config?.failOn || opts.failOn);
2638
+ const summaryText = formatCISummaryText(ciSummary);
2639
+ process.stdout.write(`
2640
+ ${summaryText}
1384
2641
  `);
1385
- process.exit(exitCode(res.severity, config?.failOn || opts.failOn));
2642
+ if (process.env.GITHUB_STEP_SUMMARY) {
2643
+ const githubSummary = formatGitHubJobSummary(ciSummary);
2644
+ await fs5.writeFile(process.env.GITHUB_STEP_SUMMARY, githubSummary, "utf8");
2645
+ }
2646
+ } else {
2647
+ process.stdout.write(`Wrote ${opts.format.toUpperCase()} report to ${resolved}
2648
+ `);
2649
+ }
2650
+ const exitCodeValue = calculateExitCode(finalResult.severity, config?.failOn || opts.failOn);
2651
+ process.exit(exitCodeValue);
1386
2652
  };
1387
2653
  main().catch((e) => {
1388
2654
  const msg = e instanceof Error ? e.message : String(e);