bun-ready 0.2.5 → 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 +114 -1
  2. package/dist/cli.js +955 -40
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -23,7 +23,7 @@ bun-ready scan .
23
23
 
24
24
  ## Usage
25
25
  ```bash
26
- bun-ready scan <path> [--format md|json] [--out <file>] [--no-install] [--no-test] [--verbose] [--detailed]
26
+ 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>]
27
27
  ```
28
28
 
29
29
  ## Examples:
@@ -238,6 +238,119 @@ Add it to the allowlist:
238
238
 
239
239
  Some packages have optional native modules that can be disabled or work fine with Bun.
240
240
 
241
+ ## v0.3 New Features
242
+
243
+ ### CI Mode
244
+
245
+ Run in CI mode for stable, machine-friendly output:
246
+
247
+ ```bash
248
+ bun-ready scan . --ci --output-dir .bun-ready-artifacts
249
+ ```
250
+
251
+ **CI Mode Benefits:**
252
+ - Reduced "human noise" in stdout
253
+ - Stable section ordering in reports
254
+ - CI summary block with top findings and next actions
255
+ - Automatic artifact generation when `--output-dir` is specified
256
+
257
+ ### SARIF Export
258
+
259
+ Generate SARIF 2.1.0 format for GitHub Code Scanning:
260
+
261
+ ```bash
262
+ bun-ready scan . --format sarif --out bun-ready.sarif.json
263
+ ```
264
+
265
+ ### Policy-as-Code
266
+
267
+ Enforce organization-specific migration policies:
268
+
269
+ ```bash
270
+ # CLI flags
271
+ bun-ready scan . --rule deps.native_addons=fail --max-warnings 5
272
+
273
+ # Or in config file
274
+ {
275
+ "rules": [
276
+ { "id": "deps.native_addons", "action": "fail" },
277
+ { "id": "scripts.lifecycle", "action": "off" }
278
+ ],
279
+ "thresholds": {
280
+ "maxWarnings": 10,
281
+ "maxPackagesRed": 2
282
+ }
283
+ }
284
+ ```
285
+
286
+ **Policy Sources** (priority order):
287
+ 1. CLI flags (`--rule`, `--max-warnings`)
288
+ 2. `bun-ready.config.json` file
289
+ 3. Default rules
290
+
291
+ **Policy Actions:**
292
+ - `fail` - Mark finding as failure
293
+ - `warn` - Downgrade to warning
294
+ - `off` - Disable this finding
295
+ - `ignore` - Ignore this finding (don't report)
296
+
297
+ ### Baseline / Regression Detection
298
+
299
+ Prevent regressions by comparing against a baseline:
300
+
301
+ ```bash
302
+ # Create baseline (first time)
303
+ bun-ready scan . --baseline bun-ready-baseline.json --update-baseline
304
+
305
+ # In CI - compare against baseline
306
+ bun-ready scan . --baseline bun-ready-baseline.json --ci
307
+ ```
308
+
309
+ **Baseline Features:**
310
+ - Detects new findings
311
+ - Detects resolved findings
312
+ - Detects severity changes (upgrades/downgrades)
313
+ - Regression verdict: fails if new red findings or increased failures
314
+ - Update baseline with `--update-baseline`
315
+
316
+ ### Changed-Only Scanning (Monorepos)
317
+
318
+ Scan only changed packages to speed up large monorepo PRs:
319
+
320
+ ```bash
321
+ bun-ready scan . --changed-only --since main
322
+ ```
323
+
324
+ **Changed-Only Verdict Types:**
325
+ - **Partial verdict** (no baseline): Warning about partial coverage
326
+ - **Regression verdict** (with baseline): OK - comparing only changed packages
327
+
328
+ ### GitHub Action Integration
329
+
330
+ Use bun-ready as a GitHub Action:
331
+
332
+ ```yaml
333
+ name: bun-ready Check
334
+ on: [pull_request]
335
+ jobs:
336
+ bun-ready:
337
+ runs-on: ubuntu-latest
338
+ steps:
339
+ - uses: actions/checkout@v4
340
+ - uses: oven-sh/setup-bun@v1
341
+ - uses: ./ # Uses action.yml from repo
342
+ with:
343
+ fail-on: yellow
344
+ baseline: bun-ready-baseline.json
345
+ ```
346
+
347
+ **Action Features:**
348
+ - Automatic artifact upload
349
+ - GitHub job summary generation
350
+ - PR comment support
351
+ - SARIF export for Code Scanning
352
+ - Policy and baseline support
353
+
241
354
  ## What it checks (MVP)
242
355
  - package.json presence & shape
243
356
  - lockfiles (npm/yarn/pnpm/bun)
package/dist/cli.js CHANGED
@@ -86,9 +86,127 @@ var init_bun_check = __esm(() => {
86
86
  init_spawn();
87
87
  });
88
88
 
89
- // src/cli.ts
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
+ });
90
99
  import { promises as fs4 } from "node:fs";
91
- import path6 from "node:path";
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
+
207
+ // src/cli.ts
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();
@@ -1272,6 +1390,119 @@ var formatPackageStats = (pkg) => {
1272
1390
  }
1273
1391
  return lines;
1274
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
+ `);
1505
+ };
1275
1506
  function renderMarkdown(r) {
1276
1507
  const lines = [];
1277
1508
  const bunVersion = process.version;
@@ -1323,6 +1554,9 @@ function renderMarkdown(r) {
1323
1554
  }
1324
1555
  lines.push(``);
1325
1556
  }
1557
+ if (r.policyApplied) {
1558
+ lines.push(formatPolicyApplied(r.policyApplied));
1559
+ }
1326
1560
  lines.push(`## Root Package`);
1327
1561
  lines.push(`- Path: \`${r.repo.packageJsonPath.replace(/\\/g, "/")}\``);
1328
1562
  lines.push(`- Workspaces: ${r.repo.hasWorkspaces ? "yes" : "no"}`);
@@ -1393,6 +1627,13 @@ function renderMarkdown(r) {
1393
1627
  }
1394
1628
  }
1395
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
+ }
1396
1637
  if (r.packages && r.packages.length > 0) {
1397
1638
  const sortedPackages = stableSort(r.packages, (p) => p.name);
1398
1639
  for (const pkg of sortedPackages) {
@@ -1473,6 +1714,9 @@ var renderDetailedReport = (r) => {
1473
1714
  lines.push(``);
1474
1715
  lines.push(`**Overall:** ${badge(r.severity)}`);
1475
1716
  lines.push(``);
1717
+ if (r.policyApplied) {
1718
+ lines.push(formatPolicyApplied(r.policyApplied));
1719
+ }
1476
1720
  lines.push(`## Detailed Package Usage`);
1477
1721
  lines.push(``);
1478
1722
  let hasUsageInfo = false;
@@ -1535,6 +1779,13 @@ var renderDetailedReport = (r) => {
1535
1779
  }
1536
1780
  }
1537
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
+ }
1538
1789
  return lines.join(`
1539
1790
  `);
1540
1791
  };
@@ -1544,16 +1795,512 @@ function renderJson(r) {
1544
1795
  return JSON.stringify(r, null, 2);
1545
1796
  }
1546
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
+
1547
2294
  // src/cli.ts
1548
2295
  var usage = () => {
1549
2296
  return [
1550
2297
  "bun-ready",
1551
2298
  "",
1552
2299
  "Usage:",
1553
- " bun-ready scan <path> [--format md|json] [--out <file>] [--no-install] [--no-test] [--verbose] [--detailed] [--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>]",
1554
2301
  "",
1555
2302
  "Options:",
1556
- " --format md|json Output format (default: md)",
2303
+ " --format md|json|sarif Output format (default: md)",
1557
2304
  " --out <file> Output file path (default: bun-ready.md or bun-ready.json)",
1558
2305
  " --no-install Skip bun install --dry-run",
1559
2306
  " --no-test Skip bun test",
@@ -1561,6 +2308,14 @@ var usage = () => {
1561
2308
  " --detailed Show detailed package usage report with file paths",
1562
2309
  " --scope root|packages|all Scan scope for monorepos (default: all)",
1563
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)",
1564
2319
  "",
1565
2320
  "Exit codes:",
1566
2321
  " 0 green",
@@ -1597,11 +2352,17 @@ var parseArgs = (argv) => {
1597
2352
  let detailed = false;
1598
2353
  let scope = "all";
1599
2354
  let failOn;
2355
+ let ci;
2356
+ let outputDir;
2357
+ let ruleArgs = [];
2358
+ let maxWarnings;
2359
+ let baseline;
2360
+ let changedOnly;
1600
2361
  for (let i = 2;i < args.length; i++) {
1601
2362
  const a = args[i] ?? "";
1602
2363
  if (a === "--format") {
1603
2364
  const v = args[i + 1] ?? "";
1604
- if (v === "md" || v === "json")
2365
+ if (v === "md" || v === "json" || v === "sarif")
1605
2366
  format = v;
1606
2367
  i++;
1607
2368
  continue;
@@ -1641,6 +2402,59 @@ var parseArgs = (argv) => {
1641
2402
  i++;
1642
2403
  continue;
1643
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
+ }
1644
2458
  }
1645
2459
  const baseOpts = {
1646
2460
  repoPath,
@@ -1655,35 +2469,36 @@ var parseArgs = (argv) => {
1655
2469
  if (failOn !== undefined) {
1656
2470
  baseOpts.failOn = failOn;
1657
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
+ }
1658
2497
  return {
1659
2498
  cmd,
1660
2499
  opts: baseOpts
1661
2500
  };
1662
2501
  };
1663
- var exitCode = (sev, failOn) => {
1664
- if (!failOn) {
1665
- if (sev === "green")
1666
- return 0;
1667
- if (sev === "yellow")
1668
- return 2;
1669
- return 3;
1670
- }
1671
- if (failOn === "green") {
1672
- if (sev === "green")
1673
- return 0;
1674
- return 3;
1675
- }
1676
- if (failOn === "yellow") {
1677
- if (sev === "red")
1678
- return 3;
1679
- return 0;
1680
- }
1681
- if (sev === "green")
1682
- return 0;
1683
- if (sev === "yellow")
1684
- return 2;
1685
- return 3;
1686
- };
1687
2502
  var main = async () => {
1688
2503
  const { cmd, opts } = parseArgs(process.argv);
1689
2504
  if (cmd !== "scan") {
@@ -1699,6 +2514,7 @@ var main = async () => {
1699
2514
  configOpts.detailed = opts.detailed;
1700
2515
  }
1701
2516
  const config = await mergeConfigWithOpts(null, configOpts);
2517
+ const mergedPolicy = mergePolicyConfigs(opts.policy, config?.rules ? { rules: config.rules } : undefined);
1702
2518
  const scanOpts = {
1703
2519
  repoPath: opts.repoPath,
1704
2520
  format: opts.format,
@@ -1711,14 +2527,78 @@ var main = async () => {
1711
2527
  if (opts.failOn !== undefined) {
1712
2528
  scanOpts.failOn = opts.failOn;
1713
2529
  }
2530
+ if (opts.ci !== undefined) {
2531
+ scanOpts.ci = opts.ci;
2532
+ }
2533
+ if (mergedPolicy !== undefined) {
2534
+ scanOpts.policy = mergedPolicy;
2535
+ }
1714
2536
  const res = await analyzeRepoOverall(scanOpts);
1715
- 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) {
1716
2596
  const skipWarnings = [];
1717
- if (res.install?.skipReason) {
1718
- skipWarnings.push(`Install check skipped: ${res.install.skipReason}`);
2597
+ if (finalResult.install?.skipReason) {
2598
+ skipWarnings.push(`Install check skipped: ${finalResult.install.skipReason}`);
1719
2599
  }
1720
- if (res.test?.skipReason) {
1721
- skipWarnings.push(`Test run skipped: ${res.test.skipReason}`);
2600
+ if (finalResult.test?.skipReason) {
2601
+ skipWarnings.push(`Test run skipped: ${finalResult.test.skipReason}`);
1722
2602
  }
1723
2603
  if (skipWarnings.length > 0) {
1724
2604
  process.stderr.write(`WARNING:
@@ -1727,13 +2607,48 @@ ${skipWarnings.map((w) => ` - ${w}`).join(`
1727
2607
  `);
1728
2608
  }
1729
2609
  }
1730
- const out = opts.format === "json" ? renderJson(res) : opts.detailed ? renderDetailedReport(res) : renderMarkdown(res);
1731
- const target = opts.outFile ?? (opts.format === "json" ? "bun-ready.json" : opts.detailed ? "bun-ready-detailed.md" : "bun-ready.md");
1732
- const resolved = path6.resolve(process.cwd(), target);
1733
- await fs4.writeFile(resolved, out, "utf8");
1734
- 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}
1735
2641
  `);
1736
- 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);
1737
2652
  };
1738
2653
  main().catch((e) => {
1739
2654
  const msg = e instanceof Error ? e.message : String(e);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bun-ready",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "CLI that estimates how painful migrating a Node.js repo to Bun might be. Generates a green/yellow/red Markdown report with reasons.",
5
5
  "author": "Pas7 Studio",
6
6
  "license": "Apache-2.0",