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.
- package/README.md +178 -5
- package/dist/cli.js +1345 -79
- 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
|
|
91
|
-
import
|
|
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
|
|
213
|
+
import path5 from "node:path";
|
|
96
214
|
import os from "node:os";
|
|
97
|
-
import { promises as
|
|
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
|
|
831
|
+
import path3 from "node:path";
|
|
547
832
|
import fsSync from "node:fs";
|
|
548
|
-
function globMatch(pattern,
|
|
833
|
+
function globMatch(pattern, path4) {
|
|
549
834
|
const patternParts = pattern.split("/");
|
|
550
|
-
const pathParts =
|
|
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 =
|
|
873
|
+
const patternPath = path3.resolve(rootPath, pattern);
|
|
589
874
|
if (pattern.includes("/*") && !pattern.includes("/**")) {
|
|
590
875
|
try {
|
|
591
|
-
const baseDir =
|
|
592
|
-
const patternName =
|
|
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 =
|
|
597
|
-
const pkgJsonPath =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
734
|
-
bunLockb: await fileExists(
|
|
735
|
-
npmLock: await fileExists(
|
|
736
|
-
yarnLock: await fileExists(
|
|
737
|
-
pnpmLock: await fileExists(
|
|
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
|
|
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
|
|
1052
|
+
const base = await fs3.mkdtemp(path5.join(os.tmpdir(), "bun-ready-"));
|
|
762
1053
|
const cleanup = async () => {
|
|
763
1054
|
try {
|
|
764
|
-
await
|
|
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(
|
|
771
|
-
await copyIfExists(
|
|
772
|
-
await copyIfExists(
|
|
773
|
-
await copyIfExists(
|
|
774
|
-
await copyIfExists(
|
|
775
|
-
await copyIfExists(
|
|
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 ||
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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} | \`${
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
1367
|
-
skipWarnings.push(`Install check skipped: ${
|
|
2597
|
+
if (finalResult.install?.skipReason) {
|
|
2598
|
+
skipWarnings.push(`Install check skipped: ${finalResult.install.skipReason}`);
|
|
1368
2599
|
}
|
|
1369
|
-
if (
|
|
1370
|
-
skipWarnings.push(`Test run skipped: ${
|
|
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
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
-
|
|
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);
|