bun-ready 0.1.0 → 0.2.3

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 +139 -0
  2. package/dist/cli.js +1024 -145
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,13 +1,14 @@
1
- #!/usr/bin/env node
2
-
3
- // src/cli.ts
4
- import { promises as fs3 } from "node:fs";
5
- import path3 from "node:path";
6
-
7
- // src/analyze.ts
8
- import path2 from "node:path";
9
- import os from "node:os";
10
- import { promises as fs2 } from "node:fs";
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, {
5
+ get: all[name],
6
+ enumerable: true,
7
+ configurable: true,
8
+ set: (newValue) => all[name] = () => newValue
9
+ });
10
+ };
11
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
11
12
 
12
13
  // src/spawn.ts
13
14
  import { spawn } from "node:child_process";
@@ -20,8 +21,7 @@ var runNodeSpawn = async (cmd, args, cwd) => {
20
21
  child.stderr.on("data", (d) => err += String(d));
21
22
  child.on("close", (code) => resolve({ code: code ?? 1, stdout: out, stderr: err }));
22
23
  });
23
- };
24
- var runBunSpawn = async (cmd, args, cwd) => {
24
+ }, runBunSpawn = async (cmd, args, cwd) => {
25
25
  const BunAny = globalThis;
26
26
  const bun = BunAny.Bun;
27
27
  if (!bun)
@@ -48,10 +48,53 @@ var runBunSpawn = async (cmd, args, cwd) => {
48
48
  };
49
49
  const [stdout, stderr, code] = await Promise.all([readAll(proc.stdout), readAll(proc.stderr), proc.exited]);
50
50
  return { code, stdout, stderr };
51
- };
52
- var exec = async (cmd, args, cwd) => {
51
+ }, exec = async (cmd, args, cwd) => {
53
52
  return await runBunSpawn(cmd, args, cwd);
54
53
  };
54
+ var init_spawn = () => {};
55
+
56
+ // src/bun_check.ts
57
+ var exports_bun_check = {};
58
+ __export(exports_bun_check, {
59
+ checkBunAvailable: () => checkBunAvailable
60
+ });
61
+ async function checkBunAvailable() {
62
+ try {
63
+ const res = await exec("bun", ["--version"], process.cwd());
64
+ if (res.code === 0 && res.stdout.includes("bun")) {
65
+ return { available: true };
66
+ }
67
+ return {
68
+ available: false,
69
+ error: `Bun command returned unexpected output (exit ${res.code})`
70
+ };
71
+ } catch (error) {
72
+ const msg = error instanceof Error ? error.message : String(error);
73
+ if (msg.includes("ENOENT") || msg.includes("spawn bun ENOENT")) {
74
+ return {
75
+ available: false,
76
+ error: "Bun is not installed. Please install Bun from https://bun.sh or use --no-install and --no-test flags."
77
+ };
78
+ }
79
+ return {
80
+ available: false,
81
+ error: `Failed to check Bun availability: ${msg}`
82
+ };
83
+ }
84
+ }
85
+ var init_bun_check = __esm(() => {
86
+ init_spawn();
87
+ });
88
+
89
+ // src/cli.ts
90
+ import { promises as fs3 } from "node:fs";
91
+ import path5 from "node:path";
92
+
93
+ // src/analyze.ts
94
+ init_spawn();
95
+ import path4 from "node:path";
96
+ import os from "node:os";
97
+ import { promises as fs2 } from "node:fs";
55
98
 
56
99
  // src/util.ts
57
100
  import { promises as fs } from "node:fs";
@@ -81,7 +124,7 @@ var truncateLines = (lines, max) => {
81
124
  };
82
125
 
83
126
  // src/heuristics.ts
84
- var NATIVE_SUSPECTS = [
127
+ var NATIVE_SUSPECTS_V2 = [
85
128
  "node-gyp",
86
129
  "node-pre-gyp",
87
130
  "prebuild-install",
@@ -97,7 +140,112 @@ var NATIVE_SUSPECTS = [
97
140
  "argon2",
98
141
  "bufferutil",
99
142
  "utf-8-validate",
100
- "fsevents"
143
+ "fsevents",
144
+ "grpc",
145
+ "@grpc/grpc-js",
146
+ "grpc-js",
147
+ "bcryptjs",
148
+ "bcrypt",
149
+ "sodium",
150
+ "libsodium",
151
+ "leveldb",
152
+ "level",
153
+ "rocksdb",
154
+ "mysql2",
155
+ "pg",
156
+ "oracledb",
157
+ "nodegit",
158
+ "ffi-napi",
159
+ "node-ffi",
160
+ "ref-napi",
161
+ "skia-canvas",
162
+ "jimp",
163
+ "pdfkit",
164
+ "sharp",
165
+ "pixelmatch",
166
+ "cheerio",
167
+ "node-wav",
168
+ "lamejs",
169
+ "flac-bindings",
170
+ "opus-recorder",
171
+ "silk-wasm",
172
+ "zeromq",
173
+ "zeromq.js",
174
+ "mongodb",
175
+ "redis",
176
+ "ioredis",
177
+ "elasticsearch",
178
+ "snappy",
179
+ "snappyjs",
180
+ "iltorb",
181
+ "brotli",
182
+ "node-sha3",
183
+ "ursa",
184
+ "node-forge",
185
+ "jsonwebtoken",
186
+ "node-cron",
187
+ "bull",
188
+ "bullmq"
189
+ ];
190
+ var DEV_TOOL_SUSPECTS = [
191
+ "jest",
192
+ "vitest",
193
+ "mocha",
194
+ "chai",
195
+ "ava",
196
+ "tap",
197
+ "jasmine",
198
+ "karma",
199
+ "cypress",
200
+ "playwright",
201
+ "puppeteer",
202
+ "selenium-webdriver",
203
+ "webdriverio",
204
+ "nightwatch",
205
+ "testcafe",
206
+ "protractor"
207
+ ];
208
+ var RUNTIME_TOOL_SUSPECTS = [
209
+ "ts-node",
210
+ "tsx",
211
+ "ts-node-dev",
212
+ "nodemon",
213
+ "babel",
214
+ "@babel/core",
215
+ "@babel/node",
216
+ "babel-cli",
217
+ "babel-register",
218
+ "babel-preset-env",
219
+ "webpack",
220
+ "webpack-cli",
221
+ "rollup",
222
+ "@rollup/plugin",
223
+ "esbuild",
224
+ "vite",
225
+ "@vitejs/plugin",
226
+ "swc",
227
+ "@swc/core",
228
+ "@swc/register",
229
+ "turbopack",
230
+ "snowpack",
231
+ "parcel",
232
+ "browserify"
233
+ ];
234
+ var PM_SPECIFIC_COMMANDS = [
235
+ "npm ci",
236
+ "npm run ci",
237
+ "pnpm -r",
238
+ "pnpm recursive",
239
+ "pnpm workspaces",
240
+ "yarn workspaces",
241
+ "yarn workspace",
242
+ "yarn -w",
243
+ "lerna run",
244
+ "nx run",
245
+ "npx lerna",
246
+ "npx nx",
247
+ "turbo run",
248
+ "rushx"
101
249
  ];
102
250
  var includesAny = (s, needles) => {
103
251
  const lower = s.toLowerCase();
@@ -144,31 +292,6 @@ var detectScriptRisks = (repo) => {
144
292
  }
145
293
  return findings;
146
294
  };
147
- var detectNativeAddonRisk = (repo) => {
148
- const allDeps = {
149
- ...repo.dependencies,
150
- ...repo.devDependencies,
151
- ...repo.optionalDependencies
152
- };
153
- const names = Object.keys(allDeps);
154
- const suspects = stableSort(names.filter((n) => NATIVE_SUSPECTS.includes(n) || includesAny(n, ["napi", "node-gyp", "prebuild", "ffi"])), (x) => x);
155
- if (suspects.length === 0)
156
- return [];
157
- const hardRed = suspects.some((n) => n === "node-gyp" || n === "node-sass");
158
- const severity = hardRed ? "red" : "yellow";
159
- return [
160
- {
161
- id: "deps.native_addons",
162
- title: "Potential native addons / node-gyp toolchain risk",
163
- severity,
164
- details: suspects.map((n) => `${n}@${allDeps[n] ?? ""}`.trim()),
165
- hints: [
166
- "Native addons often require toolchains and can be sensitive to runtime differences.",
167
- "If you see install/build failures, try upgrading these packages or switching to pure-JS alternatives."
168
- ]
169
- }
170
- ];
171
- };
172
295
  var detectLockfileSignals = (repo) => {
173
296
  const { bunLock, bunLockb, npmLock, yarnLock, pnpmLock } = repo.lockfiles;
174
297
  if (bunLock || bunLockb)
@@ -208,6 +331,153 @@ var detectLockfileSignals = (repo) => {
208
331
  }
209
332
  ];
210
333
  };
334
+ var detectRuntimeApiRisks = (repo) => {
335
+ const findings = [];
336
+ if (repo.packageJson?.engines?.node) {
337
+ const nodeVersion = repo.packageJson.engines.node;
338
+ const match = nodeVersion.match(/>=?(\d+)/);
339
+ if (match && match[1]) {
340
+ const minVersion = parseInt(match[1], 10);
341
+ if (minVersion < 18) {
342
+ findings.push({
343
+ id: "runtime.node_version",
344
+ title: "Node.js version requirement is below Bun's baseline (v18+)",
345
+ severity: "yellow",
346
+ details: [`engines.node: ${nodeVersion}`],
347
+ hints: [
348
+ "Bun targets Node 18+ compatibility. Packages requiring older Node versions may need updates.",
349
+ "Check if packages have updates or if version constraints can be relaxed."
350
+ ]
351
+ });
352
+ }
353
+ }
354
+ }
355
+ const allDeps = { ...repo.dependencies, ...repo.devDependencies, ...repo.optionalDependencies };
356
+ const deps = Object.keys(allDeps);
357
+ const devToolHits = deps.filter((d) => DEV_TOOL_SUSPECTS.includes(d) || deps.some((x) => x.startsWith(`${d}/`)));
358
+ const relevantDevTools = devToolHits.filter((d) => allDeps[d]);
359
+ if (relevantDevTools.length > 0) {
360
+ findings.push({
361
+ id: "runtime.dev_tools",
362
+ title: "Dev tools that may need Bun compatibility checks",
363
+ severity: "yellow",
364
+ details: relevantDevTools.map((d) => `${d}@${allDeps[d]}`),
365
+ hints: [
366
+ "Testing frameworks like jest/vitest may work, but consider migrating to bun:test for optimal performance.",
367
+ "Build tools like webpack/esbuild/vite typically work with Bun, but verify your build pipeline.",
368
+ "Check documentation for each tool's Bun compatibility status."
369
+ ]
370
+ });
371
+ }
372
+ const runtimeToolHits = deps.filter((d) => RUNTIME_TOOL_SUSPECTS.includes(d) || deps.some((x) => x.startsWith(`${d}/`)));
373
+ const relevantRuntimeTools = runtimeToolHits.filter((d) => allDeps[d]);
374
+ if (relevantRuntimeTools.length > 0) {
375
+ findings.push({
376
+ id: "runtime.build_tools",
377
+ title: "Runtime/build tools that may need Bun compatibility verification",
378
+ severity: "yellow",
379
+ details: relevantRuntimeTools.map((d) => `${d}@${allDeps[d]}`),
380
+ hints: [
381
+ "Tools like ts-node/tsx work with Bun, but verify your TypeScript configuration.",
382
+ "Bundlers like webpack/vite/esbuild typically work with Bun, but verify your build scripts.",
383
+ "Consider switching to Bun's native build capabilities for improved performance."
384
+ ]
385
+ });
386
+ }
387
+ const scriptNames = Object.keys(repo.scripts);
388
+ const tsRuntimeScripts = scriptNames.filter((k) => {
389
+ const script = repo.scripts[k]?.toLowerCase() || "";
390
+ return script.includes("ts-node") || script.includes("tsx") || script.includes("babel-node");
391
+ });
392
+ if (tsRuntimeScripts.length > 0) {
393
+ findings.push({
394
+ id: "runtime.ts_execution",
395
+ title: "Scripts use TypeScript runtime execution (ts-node/tsx/babel-node)",
396
+ severity: "yellow",
397
+ details: tsRuntimeScripts.map((k) => `${k}: ${repo.scripts[k]}`),
398
+ hints: [
399
+ "Bun supports TypeScript natively, but verify tsconfig compatibility.",
400
+ "Consider migrating scripts to use `bun run` directly with TypeScript files.",
401
+ "Test execution of affected scripts with Bun before full migration."
402
+ ]
403
+ });
404
+ }
405
+ return findings;
406
+ };
407
+ var detectPmAssumptions = (repo) => {
408
+ const findings = [];
409
+ const scriptNames = Object.keys(repo.scripts);
410
+ const pmSpecificHits = [];
411
+ for (const scriptName of scriptNames) {
412
+ const script = repo.scripts[scriptName] || "";
413
+ const lowerScript = script.toLowerCase();
414
+ for (const cmd of PM_SPECIFIC_COMMANDS) {
415
+ if (lowerScript.includes(cmd.toLowerCase())) {
416
+ pmSpecificHits.push({ name: scriptName, command: cmd });
417
+ break;
418
+ }
419
+ }
420
+ }
421
+ if (pmSpecificHits.length > 0) {
422
+ findings.push({
423
+ id: "scripts.pm_assumptions",
424
+ title: "Scripts contain package-manager-specific commands",
425
+ severity: "yellow",
426
+ details: pmSpecificHits.map((h) => `${h.name}: uses "${h.command}"`),
427
+ hints: [
428
+ "Package-manager-specific commands may need adaptation for Bun.",
429
+ "Test each affected script with Bun to ensure compatibility.",
430
+ "Consider rewriting scripts to be package-manager agnostic where possible."
431
+ ]
432
+ });
433
+ }
434
+ return findings;
435
+ };
436
+ var detectNativeAddonRiskV2 = (repo, config) => {
437
+ const allDeps = {
438
+ ...repo.dependencies,
439
+ ...repo.devDependencies,
440
+ ...repo.optionalDependencies
441
+ };
442
+ const names = Object.keys(allDeps);
443
+ const allowlist = config?.nativeAddonAllowlist || [];
444
+ const suspects = names.filter((n) => {
445
+ if (allowlist.includes(n))
446
+ return false;
447
+ const inList = NATIVE_SUSPECTS_V2.includes(n);
448
+ if (inList)
449
+ return true;
450
+ const keywords = ["@napi-rs/", "napi-rs", "node-napi", "neon", "node-gyp", "prebuild", "ffi", "bindings", "native", "native-module"];
451
+ const keywordMatch = includesAny(n, keywords);
452
+ if (keywordMatch) {
453
+ return true;
454
+ }
455
+ return false;
456
+ });
457
+ const scriptNames = Object.keys(repo.scripts);
458
+ const hasNodeGypRebuild = scriptNames.some((k) => {
459
+ const script = repo.scripts[k]?.toLowerCase() || "";
460
+ return script.includes("node-gyp") || script.includes("node-gyp rebuild");
461
+ });
462
+ if (suspects.length === 0 && !hasNodeGypRebuild)
463
+ return [];
464
+ const hardRed = suspects.some((n) => n === "node-gyp" || n === "node-sass") || hasNodeGypRebuild;
465
+ const severity = hardRed ? "red" : "yellow";
466
+ return [
467
+ {
468
+ id: "deps.native_addons",
469
+ title: "Potential native addons / node-gyp toolchain risk",
470
+ severity,
471
+ details: suspects.map((n) => `${n}@${allDeps[n]}`),
472
+ hints: [
473
+ "Native addons often require toolchains and can be sensitive to runtime differences.",
474
+ "If you see install/build failures, try upgrading these packages or switching to pure-JS alternatives.",
475
+ "Some packages offer optional native modules that can be disabled via configuration.",
476
+ "Check if native modules are in use or just installed for optional features."
477
+ ]
478
+ }
479
+ ];
480
+ };
211
481
  var summarizeSeverity = (findings, installOk, testOk) => {
212
482
  let sev = "green";
213
483
  for (const f of findings)
@@ -219,33 +489,276 @@ var summarizeSeverity = (findings, installOk, testOk) => {
219
489
  return sev;
220
490
  };
221
491
 
492
+ // src/bun_logs.ts
493
+ function parseInstallLogs(logs) {
494
+ const blockedDeps = [];
495
+ const trustedDepsMentioned = [];
496
+ const notes = [];
497
+ const blockedKeywords = ["blocked", "not allowed", "lifecycle script", "postinstall blocked", "prepare blocked"];
498
+ const trustedKeywords = ["trustedDependencies", "trusted", "trust"];
499
+ for (const log of logs) {
500
+ const lowerLog = log.toLowerCase();
501
+ for (const keyword of blockedKeywords) {
502
+ if (lowerLog.includes(keyword)) {
503
+ let pkgMatch = log.match(/blocked:\s+(?:lifecycle\s+script\s+(?:for\s+)?)?(@?[a-z0-9-]+\/[a-z0-9-]+|@?[a-z0-9-]+)@[\d.^]+/i);
504
+ if (pkgMatch && pkgMatch[1] && !blockedDeps.includes(pkgMatch[1])) {
505
+ blockedDeps.push(pkgMatch[1]);
506
+ } else {
507
+ const fallbackMatch = log.match(/blocked:\s+(?:lifecycle\s+script\s+(?:for\s+)?)?(@?[a-z0-9-]+\/[a-z0-9-]+|@?[a-z0-9-]+)/i);
508
+ if (fallbackMatch && fallbackMatch[1] && !blockedDeps.includes(fallbackMatch[1]) && !blockedKeywords.includes(fallbackMatch[1].toLowerCase())) {
509
+ blockedDeps.push(fallbackMatch[1]);
510
+ }
511
+ }
512
+ break;
513
+ }
514
+ }
515
+ for (const keyword of trustedKeywords) {
516
+ if (lowerLog.includes(keyword)) {
517
+ if (!notes.some((n) => n.toLowerCase().includes(keyword))) {
518
+ notes.push(log.trim());
519
+ }
520
+ const pkgMatch = log.match(/@?[a-z0-9-]+\/[a-z0-9-]+|@?[a-z0-9-]+/gi);
521
+ if (pkgMatch) {
522
+ for (const pkg of pkgMatch) {
523
+ if (pkg.toLowerCase() !== "trusteddependencies" && !trustedDepsMentioned.includes(pkg)) {
524
+ trustedDepsMentioned.push(pkg);
525
+ }
526
+ }
527
+ }
528
+ break;
529
+ }
530
+ }
531
+ if (lowerLog.includes("warning") || lowerLog.includes("warn")) {
532
+ notes.push(log.trim());
533
+ }
534
+ }
535
+ blockedDeps.sort((a, b) => a.localeCompare(b));
536
+ trustedDepsMentioned.sort((a, b) => a.localeCompare(b));
537
+ notes.sort();
538
+ return {
539
+ blockedDeps,
540
+ trustedDepsMentioned,
541
+ notes
542
+ };
543
+ }
544
+
545
+ // src/workspaces.ts
546
+ import path2 from "node:path";
547
+ import fsSync from "node:fs";
548
+ function globMatch(pattern, path3) {
549
+ const patternParts = pattern.split("/");
550
+ const pathParts = path3.split("/");
551
+ let patternIdx = 0;
552
+ let pathIdx = 0;
553
+ while (patternIdx < patternParts.length && pathIdx < pathParts.length) {
554
+ const patternPart = patternParts[patternIdx];
555
+ const pathPart = pathParts[pathIdx];
556
+ if (patternPart === "**") {
557
+ if (patternIdx === patternParts.length - 1) {
558
+ return true;
559
+ }
560
+ const nextPatternPart = patternParts[patternIdx + 1];
561
+ while (pathIdx < pathParts.length && pathParts[pathIdx] !== nextPatternPart) {
562
+ pathIdx++;
563
+ }
564
+ patternIdx++;
565
+ } else if (patternPart === "*") {
566
+ patternIdx++;
567
+ pathIdx++;
568
+ } else {
569
+ if (patternPart !== pathPart) {
570
+ return false;
571
+ }
572
+ patternIdx++;
573
+ pathIdx++;
574
+ }
575
+ }
576
+ if (patternIdx < patternParts.length) {
577
+ const remaining = patternParts.slice(patternIdx);
578
+ if (remaining.length === 1 && remaining[0] === "**") {
579
+ return true;
580
+ }
581
+ }
582
+ return patternIdx === patternParts.length && pathIdx === pathParts.length;
583
+ }
584
+ function discoverFromWorkspaces(rootPath, workspaces) {
585
+ const patterns = Array.isArray(workspaces) ? workspaces : workspaces.packages;
586
+ const packages = [];
587
+ for (const pattern of patterns) {
588
+ const patternPath = path2.resolve(rootPath, pattern);
589
+ if (pattern.includes("/*") && !pattern.includes("/**")) {
590
+ try {
591
+ const baseDir = path2.dirname(patternPath);
592
+ const patternName = path2.basename(patternPath);
593
+ const entries = fsSync.readdirSync(baseDir, { withFileTypes: true });
594
+ for (const entry of entries) {
595
+ if (entry.isDirectory() && globMatch(patternName, entry.name)) {
596
+ const packagePath = path2.join(baseDir, entry.name);
597
+ const pkgJsonPath = path2.join(packagePath, "package.json");
598
+ if (fileExistsSync(pkgJsonPath)) {
599
+ packages.push(packagePath);
600
+ }
601
+ }
602
+ }
603
+ } catch {}
604
+ }
605
+ }
606
+ return packages;
607
+ }
608
+ function fileExistsSync(filePath) {
609
+ try {
610
+ fsSync.accessSync(filePath);
611
+ return true;
612
+ } catch {
613
+ return false;
614
+ }
615
+ }
616
+ async function discoverWorkspaces(rootPath) {
617
+ const packages = [];
618
+ const rootPkgJson = path2.join(rootPath, "package.json");
619
+ if (!await fileExists(rootPkgJson)) {
620
+ return packages;
621
+ }
622
+ const rootPkg = await readJsonFile(rootPkgJson);
623
+ if (!rootPkg.workspaces && !rootPkg.packages) {
624
+ return packages;
625
+ }
626
+ const workspaces = rootPkg.workspaces || rootPkg.packages;
627
+ if (!workspaces) {
628
+ return packages;
629
+ }
630
+ const packagePaths = discoverFromWorkspaces(rootPath, workspaces);
631
+ for (const pkgPath of packagePaths) {
632
+ const pkgJsonPath = path2.join(pkgPath, "package.json");
633
+ try {
634
+ const pkg = await readJsonFile(pkgJsonPath);
635
+ if (pkg.name) {
636
+ packages.push({
637
+ name: pkg.name,
638
+ path: pkgPath,
639
+ packageJsonPath: pkgJsonPath
640
+ });
641
+ }
642
+ } catch {
643
+ continue;
644
+ }
645
+ }
646
+ packages.sort((a, b) => a.name.localeCompare(b.name));
647
+ return packages;
648
+ }
649
+ async function hasWorkspaces(rootPath) {
650
+ const rootPkgJson = path2.join(rootPath, "package.json");
651
+ if (!await fileExists(rootPkgJson)) {
652
+ return false;
653
+ }
654
+ const rootPkg = await readJsonFile(rootPkgJson);
655
+ return Boolean(rootPkg.workspaces || rootPkg.packages);
656
+ }
657
+
658
+ // src/config.ts
659
+ import path3 from "node:path";
660
+ var CONFIG_FILE_NAME = "bun-ready.config.json";
661
+ async function readConfig(rootPath) {
662
+ const configPath = path3.join(rootPath, CONFIG_FILE_NAME);
663
+ if (!await fileExists(configPath)) {
664
+ return null;
665
+ }
666
+ try {
667
+ const config = await readJsonFile(configPath);
668
+ return validateConfig(config);
669
+ } catch (error) {
670
+ const msg = error instanceof Error ? error.message : String(error);
671
+ process.stderr.write(`Warning: Invalid ${CONFIG_FILE_NAME}: ${msg}
672
+ `);
673
+ return null;
674
+ }
675
+ }
676
+ function validateConfig(config) {
677
+ if (!config || typeof config !== "object") {
678
+ return null;
679
+ }
680
+ const cfg = config;
681
+ const result = {};
682
+ if (Array.isArray(cfg.ignorePackages)) {
683
+ const validIgnore = cfg.ignorePackages.filter((p) => typeof p === "string");
684
+ if (validIgnore.length > 0) {
685
+ result.ignorePackages = validIgnore;
686
+ }
687
+ }
688
+ if (Array.isArray(cfg.ignoreFindings)) {
689
+ const validIgnore = cfg.ignoreFindings.filter((f) => typeof f === "string");
690
+ if (validIgnore.length > 0) {
691
+ result.ignoreFindings = validIgnore;
692
+ }
693
+ }
694
+ if (Array.isArray(cfg.nativeAddonAllowlist)) {
695
+ const validAllowlist = cfg.nativeAddonAllowlist.filter((p) => typeof p === "string");
696
+ if (validAllowlist.length > 0) {
697
+ result.nativeAddonAllowlist = validAllowlist;
698
+ }
699
+ }
700
+ if (typeof cfg.failOn === "string") {
701
+ const validFailOn = ["green", "yellow", "red"].includes(cfg.failOn);
702
+ if (validFailOn) {
703
+ result.failOn = cfg.failOn;
704
+ }
705
+ }
706
+ if (Object.keys(result).length === 0) {
707
+ return null;
708
+ }
709
+ return result;
710
+ }
711
+ function mergeConfigWithOpts(config, opts) {
712
+ if (!config && !opts.failOn) {
713
+ return null;
714
+ }
715
+ const result = {
716
+ ...config || {}
717
+ };
718
+ if (opts.failOn) {
719
+ result.failOn = opts.failOn;
720
+ }
721
+ return Object.keys(result).length > 0 ? result : null;
722
+ }
723
+
222
724
  // src/analyze.ts
223
- var readRepoInfo = async (repoPath) => {
224
- const packageJsonPath = path2.join(repoPath, "package.json");
725
+ async function readRepoInfo(packagePath) {
726
+ const packageJsonPath = path4.join(packagePath, "package.json");
225
727
  const pkg = await readJsonFile(packageJsonPath);
226
728
  const scripts = pkg.scripts ?? {};
227
729
  const dependencies = pkg.dependencies ?? {};
228
730
  const devDependencies = pkg.devDependencies ?? {};
229
731
  const optionalDependencies = pkg.optionalDependencies ?? {};
230
- const hasWorkspaces = Boolean(pkg.workspaces);
231
732
  const lockfiles = {
232
- bunLock: await fileExists(path2.join(repoPath, "bun.lock")),
233
- bunLockb: await fileExists(path2.join(repoPath, "bun.lockb")),
234
- npmLock: await fileExists(path2.join(repoPath, "package-lock.json")),
235
- yarnLock: await fileExists(path2.join(repoPath, "yarn.lock")),
236
- pnpmLock: await fileExists(path2.join(repoPath, "pnpm-lock.yaml"))
733
+ bunLock: await fileExists(path4.join(packagePath, "bun.lock")),
734
+ bunLockb: await fileExists(path4.join(packagePath, "bun.lockb")),
735
+ npmLock: await fileExists(path4.join(packagePath, "package-lock.json")),
736
+ yarnLock: await fileExists(path4.join(packagePath, "yarn.lock")),
737
+ pnpmLock: await fileExists(path4.join(packagePath, "pnpm-lock.yaml"))
237
738
  };
238
- return { packageJsonPath, lockfiles, scripts, dependencies, devDependencies, optionalDependencies, hasWorkspaces };
239
- };
240
- var copyIfExists = async (from, to) => {
739
+ return { pkg, scripts, dependencies, devDependencies, optionalDependencies, lockfiles };
740
+ }
741
+ async function copyIfExists(from, to) {
241
742
  try {
242
743
  await fs2.copyFile(from, to);
243
744
  } catch {
244
745
  return;
245
746
  }
246
- };
247
- var runBunInstallDryRun = async (repoPath) => {
248
- const base = await fs2.mkdtemp(path2.join(os.tmpdir(), "bun-ready-"));
747
+ }
748
+ async function runBunInstallDryRun(packagePath) {
749
+ const { checkBunAvailable: checkBunAvailable2 } = await Promise.resolve().then(() => (init_bun_check(), exports_bun_check));
750
+ const bunCheck = await checkBunAvailable2();
751
+ if (!bunCheck.available) {
752
+ const skipReason = bunCheck.error;
753
+ return {
754
+ ok: false,
755
+ summary: `Skipped: ${skipReason}`,
756
+ logs: [],
757
+ installAnalysis: { blockedDeps: [], trustedDepsMentioned: [], notes: [] },
758
+ skipReason
759
+ };
760
+ }
761
+ const base = await fs2.mkdtemp(path4.join(os.tmpdir(), "bun-ready-"));
249
762
  const cleanup = async () => {
250
763
  try {
251
764
  await fs2.rm(base, { recursive: true, force: true });
@@ -254,42 +767,160 @@ var runBunInstallDryRun = async (repoPath) => {
254
767
  }
255
768
  };
256
769
  try {
257
- await copyIfExists(path2.join(repoPath, "package.json"), path2.join(base, "package.json"));
258
- await copyIfExists(path2.join(repoPath, "bun.lock"), path2.join(base, "bun.lock"));
259
- await copyIfExists(path2.join(repoPath, "bun.lockb"), path2.join(base, "bun.lockb"));
260
- await copyIfExists(path2.join(repoPath, "package-lock.json"), path2.join(base, "package-lock.json"));
261
- await copyIfExists(path2.join(repoPath, "yarn.lock"), path2.join(base, "yarn.lock"));
262
- await copyIfExists(path2.join(repoPath, "pnpm-lock.yaml"), path2.join(base, "pnpm-lock.yaml"));
770
+ await copyIfExists(path4.join(packagePath, "package.json"), path4.join(base, "package.json"));
771
+ await copyIfExists(path4.join(packagePath, "bun.lock"), path4.join(base, "bun.lock"));
772
+ await copyIfExists(path4.join(packagePath, "bun.lockb"), path4.join(base, "bun.lockb"));
773
+ await copyIfExists(path4.join(packagePath, "package-lock.json"), path4.join(base, "package-lock.json"));
774
+ await copyIfExists(path4.join(packagePath, "yarn.lock"), path4.join(base, "yarn.lock"));
775
+ await copyIfExists(path4.join(packagePath, "pnpm-lock.yaml"), path4.join(base, "pnpm-lock.yaml"));
263
776
  const res = await exec("bun", ["install", "--dry-run"], base);
264
777
  const combined = [...res.stdout ? res.stdout.split(`
265
778
  `) : [], ...res.stderr ? res.stderr.split(`
266
779
  `) : []].filter((l) => l.trim().length > 0);
267
780
  const logs = truncateLines(combined, 60);
268
- return res.code === 0 ? { ok: true, summary: "bun install --dry-run succeeded", logs } : { ok: false, summary: `bun install --dry-run failed (exit ${res.code})`, logs };
781
+ const installAnalysis = parseInstallLogs(logs);
782
+ return res.code === 0 ? { ok: true, summary: "bun install --dry-run succeeded", logs, installAnalysis } : { ok: false, summary: `bun install --dry-run failed (exit ${res.code})`, logs, installAnalysis };
269
783
  } finally {
270
784
  await cleanup();
271
785
  }
272
- };
273
- var shouldRunBunTest = (repo) => {
274
- const t = repo.scripts["test"];
786
+ }
787
+ function shouldRunBunTest(scripts) {
788
+ const t = scripts["test"];
275
789
  if (!t)
276
790
  return false;
277
791
  return t.toLowerCase().includes("bun test") || t.toLowerCase().trim() === "bun test";
278
- };
279
- var runBunTest = async (repoPath) => {
280
- const res = await exec("bun", ["test"], repoPath);
792
+ }
793
+ async function runBunTest(packagePath) {
794
+ const { checkBunAvailable: checkBunAvailable2 } = await Promise.resolve().then(() => (init_bun_check(), exports_bun_check));
795
+ const bunCheck = await checkBunAvailable2();
796
+ if (!bunCheck.available) {
797
+ const skipReason = bunCheck.error;
798
+ return {
799
+ ok: false,
800
+ summary: `Skipped: ${skipReason}`,
801
+ logs: [],
802
+ skipReason
803
+ };
804
+ }
805
+ const res = await exec("bun", ["test"], packagePath);
281
806
  const combined = [...res.stdout ? res.stdout.split(`
282
807
  `) : [], ...res.stderr ? res.stderr.split(`
283
808
  `) : []].filter((l) => l.trim().length > 0);
284
809
  const logs = truncateLines(combined, 120);
285
810
  return res.code === 0 ? { ok: true, summary: "bun test succeeded", logs } : { ok: false, summary: `bun test failed (exit ${res.code})`, logs };
286
- };
287
- var analyzeRepo = async (opts) => {
811
+ }
812
+ function filterFindings(findings, config) {
813
+ const ignoreList = config?.ignoreFindings;
814
+ if (!ignoreList || ignoreList.length === 0) {
815
+ return findings;
816
+ }
817
+ return findings.filter((f) => !ignoreList.includes(f.id));
818
+ }
819
+ async function analyzeSinglePackage(packagePath, opts, config, pkgName) {
820
+ const info = await readRepoInfo(packagePath);
821
+ const name = pkgName || info.pkg.name || path4.basename(packagePath);
822
+ let findings = [
823
+ ...detectLockfileSignals({ packageJsonPath: packagePath, lockfiles: info.lockfiles, scripts: info.scripts, dependencies: info.dependencies, devDependencies: info.devDependencies, optionalDependencies: info.optionalDependencies, hasWorkspaces: false, packageJson: info.pkg }),
824
+ ...detectScriptRisks({ packageJsonPath: packagePath, lockfiles: info.lockfiles, scripts: info.scripts, dependencies: info.dependencies, devDependencies: info.devDependencies, optionalDependencies: info.optionalDependencies, hasWorkspaces: false, packageJson: info.pkg }),
825
+ ...detectNativeAddonRiskV2({ packageJsonPath: packagePath, lockfiles: info.lockfiles, scripts: info.scripts, dependencies: info.dependencies, devDependencies: info.devDependencies, optionalDependencies: info.optionalDependencies, hasWorkspaces: false, packageJson: info.pkg }, config || undefined),
826
+ ...detectRuntimeApiRisks({ packageJsonPath: packagePath, lockfiles: info.lockfiles, scripts: info.scripts, dependencies: info.dependencies, devDependencies: info.devDependencies, optionalDependencies: info.optionalDependencies, hasWorkspaces: false, packageJson: info.pkg }),
827
+ ...detectPmAssumptions({ packageJsonPath: packagePath, lockfiles: info.lockfiles, scripts: info.scripts, dependencies: info.dependencies, devDependencies: info.devDependencies, optionalDependencies: info.optionalDependencies, hasWorkspaces: false, packageJson: info.pkg })
828
+ ];
829
+ findings = filterFindings(findings, config);
830
+ let install = null;
831
+ let installOk = null;
832
+ if (opts.runInstall) {
833
+ const installResult = await runBunInstallDryRun(packagePath);
834
+ install = {
835
+ ok: installResult.ok,
836
+ summary: installResult.summary,
837
+ logs: installResult.logs,
838
+ ...installResult.skipReason !== undefined ? { skipReason: installResult.skipReason } : {}
839
+ };
840
+ installOk = installResult.ok;
841
+ if (installResult.installAnalysis.blockedDeps.length > 0) {
842
+ findings.push({
843
+ id: "install.blocked_scripts",
844
+ title: "Lifecycle scripts blocked by Bun",
845
+ severity: "red",
846
+ details: installResult.installAnalysis.blockedDeps,
847
+ hints: [
848
+ "Bun blocks lifecycle scripts of dependencies unless they are in trustedDependencies.",
849
+ "Add the blocked packages to trustedDependencies in root package.json.",
850
+ "Review if these packages are necessary or can be replaced with alternatives."
851
+ ]
852
+ });
853
+ }
854
+ if (installResult.installAnalysis.trustedDepsMentioned.length > 0) {
855
+ findings.push({
856
+ id: "install.trusted_deps",
857
+ title: "Trusted dependencies mentioned in install output",
858
+ severity: "yellow",
859
+ details: installResult.installAnalysis.trustedDepsMentioned,
860
+ hints: [
861
+ "Review the trustedDependencies configuration in your package.json.",
862
+ "Consider adding packages to trustedDependencies if you trust them."
863
+ ]
864
+ });
865
+ }
866
+ }
867
+ let test = null;
868
+ let testOk = null;
869
+ if (opts.runTest && shouldRunBunTest(info.scripts)) {
870
+ const testResult = await runBunTest(packagePath);
871
+ test = {
872
+ ok: testResult.ok,
873
+ summary: testResult.summary,
874
+ logs: testResult.logs,
875
+ ...testResult.skipReason !== undefined ? { skipReason: testResult.skipReason } : {}
876
+ };
877
+ testOk = testResult.ok;
878
+ }
879
+ const severity = summarizeSeverity(findings, installOk, testOk);
880
+ const summaryLines = [];
881
+ summaryLines.push(`Lockfiles: ${info.lockfiles.bunLock || info.lockfiles.bunLockb ? "bun" : "non-bun or missing"}`);
882
+ summaryLines.push(`Lifecycle scripts: ${Object.keys(info.scripts).some((k) => ["postinstall", "prepare", "preinstall", "install"].includes(k)) ? "present" : "none"}`);
883
+ summaryLines.push(`Native addon risk: ${findings.some((f) => f.id === "deps.native_addons") ? "yes" : "no"}`);
884
+ summaryLines.push(`bun install dry-run: ${install ? install.ok ? "ok" : "failed" : "skipped"}`);
885
+ summaryLines.push(`bun test: ${test ? test.ok ? "ok" : "failed" : "skipped"}`);
886
+ return {
887
+ name,
888
+ path: packagePath,
889
+ severity,
890
+ summaryLines,
891
+ findings,
892
+ install,
893
+ test,
894
+ scripts: info.scripts,
895
+ dependencies: info.dependencies,
896
+ devDependencies: info.devDependencies,
897
+ optionalDependencies: info.optionalDependencies,
898
+ lockfiles: info.lockfiles
899
+ };
900
+ }
901
+ function aggregateSeverity(packages, overallSeverity) {
902
+ if (overallSeverity === "red")
903
+ return "red";
904
+ if (overallSeverity === "yellow")
905
+ return "yellow";
906
+ for (const pkg of packages) {
907
+ if (pkg.severity === "red")
908
+ return "red";
909
+ }
910
+ for (const pkg of packages) {
911
+ if (pkg.severity === "yellow")
912
+ return "yellow";
913
+ }
914
+ return "green";
915
+ }
916
+ async function analyzeRepoOverall(opts) {
288
917
  const repoPath = normalizeRepoPath(opts.repoPath);
289
- const packageJsonPath = path2.join(repoPath, "package.json");
918
+ const packageJsonPath = path4.join(repoPath, "package.json");
290
919
  const hasPkg = await fileExists(packageJsonPath);
920
+ const config = await readConfig(repoPath);
291
921
  if (!hasPkg) {
292
922
  return {
923
+ version: "0.2",
293
924
  severity: "red",
294
925
  summaryLines: ["package.json not found"],
295
926
  findings: [
@@ -305,35 +936,90 @@ var analyzeRepo = async (opts) => {
305
936
  test: null,
306
937
  repo: {
307
938
  packageJsonPath,
939
+ hasWorkspaces: false,
940
+ rootPackage: { name: "", version: "" },
308
941
  lockfiles: { bunLock: false, bunLockb: false, npmLock: false, yarnLock: false, pnpmLock: false },
309
942
  scripts: {},
310
943
  dependencies: {},
311
944
  devDependencies: {},
312
- optionalDependencies: {},
313
- hasWorkspaces: false
314
- }
945
+ optionalDependencies: {}
946
+ },
947
+ packages: [],
948
+ config
315
949
  };
316
950
  }
317
- const repo = await readRepoInfo(repoPath);
318
- const findings = [
319
- ...detectLockfileSignals(repo),
320
- ...detectScriptRisks(repo),
321
- ...detectNativeAddonRisk(repo)
322
- ];
323
- const install = opts.runInstall ? await runBunInstallDryRun(repoPath) : null;
324
- const allowTest = opts.runTest && shouldRunBunTest(repo);
325
- const test = allowTest ? await runBunTest(repoPath) : null;
326
- const installOk = install ? install.ok : null;
327
- const testOk = test ? test.ok : null;
328
- const severity = summarizeSeverity(findings, installOk, testOk);
329
- const summaryLines = [];
330
- summaryLines.push(`Lockfiles: ${repo.lockfiles.bunLock || repo.lockfiles.bunLockb ? "bun" : "non-bun or missing"}`);
331
- summaryLines.push(`Lifecycle scripts: ${Object.keys(repo.scripts).some((k) => ["postinstall", "prepare", "preinstall", "install"].includes(k)) ? "present" : "none"}`);
332
- summaryLines.push(`Native addon risk: ${findings.some((f) => f.id === "deps.native_addons") ? "yes" : "no"}`);
333
- summaryLines.push(`bun install dry-run: ${install ? install.ok ? "ok" : "failed" : "skipped"}`);
334
- summaryLines.push(`bun test: ${test ? test.ok ? "ok" : "failed" : allowTest ? "running" : "skipped"}`);
335
- return { severity, summaryLines, findings, install, test, repo };
336
- };
951
+ const rootInfo = await readRepoInfo(repoPath);
952
+ const rootHasWorkspaces = await hasWorkspaces(repoPath);
953
+ const workspacePackages = [];
954
+ if (rootHasWorkspaces) {
955
+ workspacePackages.push(...await discoverWorkspaces(repoPath));
956
+ }
957
+ let packagesToAnalyze = [];
958
+ let packagesToReport = [];
959
+ if (opts.scope === "root") {
960
+ packagesToAnalyze = [repoPath];
961
+ packagesToReport = [repoPath];
962
+ } else if (opts.scope === "packages") {
963
+ packagesToAnalyze = workspacePackages.map((wp) => wp.path);
964
+ packagesToReport = workspacePackages.map((wp) => wp.path);
965
+ } else {
966
+ packagesToAnalyze = [repoPath, ...workspacePackages.map((wp) => wp.path)];
967
+ packagesToReport = [repoPath, ...workspacePackages.map((wp) => wp.path)];
968
+ }
969
+ const ignorePackages = config?.ignorePackages;
970
+ if (ignorePackages && ignorePackages.length > 0) {
971
+ packagesToAnalyze = packagesToAnalyze.filter((p) => {
972
+ return !ignorePackages.some((ignore) => p.includes(ignore));
973
+ });
974
+ packagesToReport = packagesToReport.filter((p) => {
975
+ return !ignorePackages.some((ignore) => p.includes(ignore));
976
+ });
977
+ }
978
+ const packages = [];
979
+ for (const packagePath of packagesToAnalyze) {
980
+ const wp = workspacePackages.find((w) => w.path === packagePath);
981
+ const analysis = await analyzeSinglePackage(packagePath, opts, config, wp?.name);
982
+ packages.push(analysis);
983
+ }
984
+ const rootAnalysis = packages.find((p) => p.path === repoPath);
985
+ let overallSeverity = "green";
986
+ if (rootAnalysis) {
987
+ overallSeverity = rootAnalysis.severity;
988
+ }
989
+ overallSeverity = aggregateSeverity(packages, overallSeverity);
990
+ const overallFindings = rootAnalysis ? rootAnalysis.findings : [];
991
+ const overallSummaryLines = [];
992
+ overallSummaryLines.push(`Total packages analyzed: ${packages.length}`);
993
+ overallSummaryLines.push(`Workspaces detected: ${rootHasWorkspaces ? "yes" : "no"}`);
994
+ if (rootHasWorkspaces) {
995
+ overallSummaryLines.push(`Workspace packages: ${workspacePackages.length}`);
996
+ }
997
+ overallSummaryLines.push(`Root package severity: ${rootAnalysis ? rootAnalysis.severity : "unknown"}`);
998
+ overallSummaryLines.push(`Overall severity: ${overallSeverity}`);
999
+ return {
1000
+ version: "0.2",
1001
+ severity: overallSeverity,
1002
+ summaryLines: overallSummaryLines,
1003
+ findings: overallFindings,
1004
+ install: rootAnalysis ? rootAnalysis.install : null,
1005
+ test: rootAnalysis ? rootAnalysis.test : null,
1006
+ repo: {
1007
+ packageJsonPath,
1008
+ hasWorkspaces: rootHasWorkspaces,
1009
+ rootPackage: {
1010
+ name: rootInfo.pkg.name || "",
1011
+ version: rootInfo.pkg.version || ""
1012
+ },
1013
+ lockfiles: rootInfo.lockfiles,
1014
+ scripts: rootInfo.scripts,
1015
+ dependencies: rootInfo.dependencies,
1016
+ devDependencies: rootInfo.devDependencies,
1017
+ optionalDependencies: rootInfo.optionalDependencies
1018
+ },
1019
+ packages,
1020
+ config
1021
+ };
1022
+ }
337
1023
 
338
1024
  // src/report_md.ts
339
1025
  var badge = (s) => {
@@ -343,7 +1029,24 @@ var badge = (s) => {
343
1029
  return "\uD83D\uDFE1 YELLOW";
344
1030
  return "\uD83D\uDD34 RED";
345
1031
  };
346
- var renderMarkdown = (r) => {
1032
+ var getTopFindings = (pkg, count = 3) => {
1033
+ const sorted = [...pkg.findings].sort((a, b) => {
1034
+ const severityOrder = { red: 0, yellow: 1, green: 2 };
1035
+ if (severityOrder[a.severity] !== severityOrder[b.severity]) {
1036
+ return severityOrder[a.severity] - severityOrder[b.severity];
1037
+ }
1038
+ return a.id.localeCompare(b.id);
1039
+ });
1040
+ return sorted.slice(0, count).map((f) => `${badge(f.severity)} ${f.title}`);
1041
+ };
1042
+ var packageRow = (pkg) => {
1043
+ const name = pkg.name;
1044
+ const path5 = pkg.path.replace(/\\/g, "/");
1045
+ const severity = badge(pkg.severity);
1046
+ const topFindings = getTopFindings(pkg, 2).join(", ") || "No issues";
1047
+ return `| ${name} | \`${path5}\` | ${severity} | ${topFindings} |`;
1048
+ };
1049
+ function renderMarkdown(r) {
347
1050
  const lines = [];
348
1051
  lines.push(`# bun-ready report`);
349
1052
  lines.push(``);
@@ -353,77 +1056,157 @@ var renderMarkdown = (r) => {
353
1056
  for (const l of r.summaryLines)
354
1057
  lines.push(`- ${l}`);
355
1058
  lines.push(``);
356
- lines.push(`## Repo`);
1059
+ if (r.version) {
1060
+ lines.push(`**Report version:** ${r.version}`);
1061
+ lines.push(``);
1062
+ }
1063
+ if (r.config) {
1064
+ lines.push(`**Configuration:**`);
1065
+ const configInfo = [];
1066
+ if (r.config.ignorePackages && r.config.ignorePackages.length > 0) {
1067
+ configInfo.push(`Ignored packages: ${r.config.ignorePackages.join(", ")}`);
1068
+ }
1069
+ if (r.config.ignoreFindings && r.config.ignoreFindings.length > 0) {
1070
+ configInfo.push(`Ignored findings: ${r.config.ignoreFindings.join(", ")}`);
1071
+ }
1072
+ if (r.config.nativeAddonAllowlist && r.config.nativeAddonAllowlist.length > 0) {
1073
+ configInfo.push(`Native addon allowlist: ${r.config.nativeAddonAllowlist.join(", ")}`);
1074
+ }
1075
+ if (r.config.failOn) {
1076
+ configInfo.push(`Fail on: ${r.config.failOn}`);
1077
+ }
1078
+ if (configInfo.length > 0) {
1079
+ for (const info of configInfo) {
1080
+ lines.push(`- ${info}`);
1081
+ }
1082
+ } else {
1083
+ lines.push(`- Using default configuration`);
1084
+ }
1085
+ lines.push(``);
1086
+ }
1087
+ lines.push(`## Root Package`);
357
1088
  lines.push(`- Path: \`${r.repo.packageJsonPath.replace(/\\/g, "/")}\``);
358
1089
  lines.push(`- Workspaces: ${r.repo.hasWorkspaces ? "yes" : "no"}`);
359
- const lock = [];
360
- if (r.repo.lockfiles.bunLock)
361
- lock.push("bun.lock");
362
- if (r.repo.lockfiles.bunLockb)
363
- lock.push("bun.lockb");
364
- if (r.repo.lockfiles.npmLock)
365
- lock.push("package-lock.json");
366
- if (r.repo.lockfiles.yarnLock)
367
- lock.push("yarn.lock");
368
- if (r.repo.lockfiles.pnpmLock)
369
- lock.push("pnpm-lock.yaml");
370
- lines.push(`- Lockfiles: ${lock.length === 0 ? "none" : lock.join(", ")}`);
1090
+ lines.push(`- Name: ${r.repo.rootPackage?.name || "unknown"}`);
1091
+ lines.push(`- Version: ${r.repo.rootPackage?.version || "unknown"}`);
371
1092
  lines.push(``);
372
- if (r.install) {
1093
+ if (r.packages && (r.packages.length > 1 || r.packages.length === 1 && r.repo.hasWorkspaces)) {
1094
+ lines.push(`## Packages Overview`);
1095
+ lines.push(`| Package | Path | Status | Key Findings |`);
1096
+ lines.push(`|---------|------|--------|--------------|`);
1097
+ const sortedPackages = stableSort(r.packages, (p) => p.name);
1098
+ for (const pkg of sortedPackages) {
1099
+ lines.push(packageRow(pkg));
1100
+ }
1101
+ lines.push(``);
1102
+ }
1103
+ const rootPkg = r.packages?.find((p) => p.path === r.repo.packageJsonPath);
1104
+ if (rootPkg?.install) {
373
1105
  lines.push(`## bun install (dry-run)`);
374
- lines.push(`- Result: ${r.install.ok ? "ok" : "failed"}`);
375
- lines.push(`- Summary: ${r.install.summary}`);
376
- if (r.install.logs.length > 0) {
1106
+ lines.push(`- Result: ${rootPkg.install.ok ? "ok" : "failed"}`);
1107
+ lines.push(`- Summary: ${rootPkg.install.summary}`);
1108
+ if (rootPkg.install.logs.length > 0) {
377
1109
  lines.push(``);
378
1110
  lines.push("```text");
379
- for (const l of r.install.logs)
1111
+ for (const l of rootPkg.install.logs)
380
1112
  lines.push(l);
381
1113
  lines.push("```");
382
1114
  }
383
1115
  lines.push(``);
384
1116
  }
385
- if (r.test) {
1117
+ if (rootPkg?.test) {
386
1118
  lines.push(`## bun test`);
387
- lines.push(`- Result: ${r.test.ok ? "ok" : "failed"}`);
388
- lines.push(`- Summary: ${r.test.summary}`);
389
- if (r.test.logs.length > 0) {
1119
+ lines.push(`- Result: ${rootPkg.test.ok ? "ok" : "failed"}`);
1120
+ lines.push(`- Summary: ${rootPkg.test.summary}`);
1121
+ if (rootPkg.test.logs.length > 0) {
390
1122
  lines.push(``);
391
1123
  lines.push("```text");
392
- for (const l of r.test.logs)
1124
+ for (const l of rootPkg.test.logs)
393
1125
  lines.push(l);
394
1126
  lines.push("```");
395
1127
  }
396
1128
  lines.push(``);
397
1129
  }
398
- lines.push(`## Findings`);
1130
+ lines.push(`## Root Findings`);
399
1131
  if (r.findings.length === 0) {
400
- lines.push(`No findings. Looks good.`);
401
- lines.push(``);
402
- return lines.join(`
403
- `);
1132
+ lines.push(`No findings for root package.`);
1133
+ } else {
1134
+ const findings = stableSort(r.findings, (f) => `${f.severity}:${f.id}`);
1135
+ for (const f of findings) {
1136
+ lines.push(`### ${f.title} (${badge(f.severity)})`);
1137
+ lines.push(``);
1138
+ for (const d of f.details)
1139
+ lines.push(`- ${d}`);
1140
+ if (f.hints.length > 0) {
1141
+ lines.push(``);
1142
+ lines.push(`**Hints:**`);
1143
+ for (const h of f.hints)
1144
+ lines.push(`- ${h}`);
1145
+ }
1146
+ lines.push(``);
1147
+ }
404
1148
  }
405
- const findings = stableSort(r.findings, (f) => `${f.severity}:${f.id}`);
406
- for (const f of findings) {
407
- lines.push(`### ${f.title} (${badge(f.severity)})`);
408
- lines.push(``);
409
- for (const d of f.details)
410
- lines.push(`- ${d}`);
411
- if (f.hints.length > 0) {
1149
+ lines.push(``);
1150
+ if (r.packages && r.packages.length > 0) {
1151
+ const sortedPackages = stableSort(r.packages, (p) => p.name);
1152
+ for (const pkg of sortedPackages) {
1153
+ lines.push(`## Package: ${pkg.name} (${badge(pkg.severity)})`);
1154
+ lines.push(``);
1155
+ lines.push(`**Path:** \`${pkg.path.replace(/\\/g, "/")}\``);
1156
+ lines.push(``);
1157
+ for (const l of pkg.summaryLines)
1158
+ lines.push(`- ${l}`);
1159
+ lines.push(``);
1160
+ if (pkg.install) {
1161
+ lines.push(`**bun install (dry-run):** ${pkg.install.ok ? "ok" : "failed"}`);
1162
+ if (pkg.install.logs.length > 0 && pkg.install.logs.length < 10) {
1163
+ lines.push(``);
1164
+ lines.push("```text");
1165
+ for (const l of pkg.install.logs)
1166
+ lines.push(l);
1167
+ lines.push("```");
1168
+ }
1169
+ lines.push(``);
1170
+ }
1171
+ if (pkg.test) {
1172
+ lines.push(`**bun test:** ${pkg.test.ok ? "ok" : "failed"}`);
1173
+ if (pkg.test.logs.length > 0 && pkg.test.logs.length < 10) {
1174
+ lines.push(``);
1175
+ lines.push("```text");
1176
+ for (const l of pkg.test.logs)
1177
+ lines.push(l);
1178
+ lines.push("```");
1179
+ }
1180
+ lines.push(``);
1181
+ }
1182
+ lines.push(`**Findings:**`);
1183
+ if (pkg.findings.length === 0) {
1184
+ lines.push(`No findings for this package.`);
1185
+ } else {
1186
+ const findings = stableSort(pkg.findings, (f) => `${f.severity}:${f.id}`);
1187
+ for (const f of findings) {
1188
+ lines.push(``);
1189
+ lines.push(`### ${f.title} (${badge(f.severity)})`);
1190
+ for (const d of f.details)
1191
+ lines.push(`- ${d}`);
1192
+ if (f.hints.length > 0) {
1193
+ lines.push(`**Hints:**`);
1194
+ for (const h of f.hints)
1195
+ lines.push(`- ${h}`);
1196
+ }
1197
+ }
1198
+ }
412
1199
  lines.push(``);
413
- lines.push(`**Hints:**`);
414
- for (const h of f.hints)
415
- lines.push(`- ${h}`);
416
1200
  }
417
- lines.push(``);
418
1201
  }
419
1202
  return lines.join(`
420
1203
  `);
421
- };
1204
+ }
422
1205
 
423
1206
  // src/report_json.ts
424
- var renderJson = (r) => {
1207
+ function renderJson(r) {
425
1208
  return JSON.stringify(r, null, 2);
426
- };
1209
+ }
427
1210
 
428
1211
  // src/cli.ts
429
1212
  var usage = () => {
@@ -431,10 +1214,22 @@ var usage = () => {
431
1214
  "bun-ready",
432
1215
  "",
433
1216
  "Usage:",
434
- " bun-ready scan <path> [--format md|json] [--out <file>] [--no-install] [--no-test] [--verbose]",
1217
+ " bun-ready scan <path> [--format md|json] [--out <file>] [--no-install] [--no-test] [--verbose] [--scope root|packages|all] [--fail-on green|yellow|red]",
1218
+ "",
1219
+ "Options:",
1220
+ " --format md|json Output format (default: md)",
1221
+ " --out <file> Output file path (default: bun-ready.md or bun-ready.json)",
1222
+ " --no-install Skip bun install --dry-run",
1223
+ " --no-test Skip bun test",
1224
+ " --verbose Show detailed output",
1225
+ " --scope root|packages|all Scan scope for monorepos (default: all)",
1226
+ " --fail-on green|yellow|red Fail policy (default: red)",
435
1227
  "",
436
1228
  "Exit codes:",
437
- " 0 green, 2 yellow, 3 red"
1229
+ " 0 green",
1230
+ " 2 yellow",
1231
+ " 3 red",
1232
+ " 1 invalid command"
438
1233
  ].join(`
439
1234
  `);
440
1235
  };
@@ -444,7 +1239,15 @@ var parseArgs = (argv) => {
444
1239
  if (cmd !== "scan") {
445
1240
  return {
446
1241
  cmd,
447
- opts: { repoPath: ".", format: "md", outFile: null, runInstall: true, runTest: true, verbose: false }
1242
+ opts: {
1243
+ repoPath: ".",
1244
+ format: "md",
1245
+ outFile: null,
1246
+ runInstall: true,
1247
+ runTest: true,
1248
+ verbose: false,
1249
+ scope: "all"
1250
+ }
448
1251
  };
449
1252
  }
450
1253
  const repoPath = args[1] && !args[1].startsWith("-") ? args[1] : ".";
@@ -453,6 +1256,8 @@ var parseArgs = (argv) => {
453
1256
  let runInstall = true;
454
1257
  let runTest = true;
455
1258
  let verbose = false;
1259
+ let scope = "all";
1260
+ let failOn;
456
1261
  for (let i = 2;i < args.length; i++) {
457
1262
  const a = args[i] ?? "";
458
1263
  if (a === "--format") {
@@ -479,10 +1284,56 @@ var parseArgs = (argv) => {
479
1284
  verbose = true;
480
1285
  continue;
481
1286
  }
1287
+ if (a === "--scope") {
1288
+ const v = args[i + 1] ?? "";
1289
+ if (v === "root" || v === "packages" || v === "all")
1290
+ scope = v;
1291
+ i++;
1292
+ continue;
1293
+ }
1294
+ if (a === "--fail-on") {
1295
+ const v = args[i + 1] ?? "";
1296
+ if (v === "green" || v === "yellow" || v === "red")
1297
+ failOn = v;
1298
+ i++;
1299
+ continue;
1300
+ }
482
1301
  }
483
- return { cmd, opts: { repoPath, format, outFile, runInstall, runTest, verbose } };
1302
+ const baseOpts = {
1303
+ repoPath,
1304
+ format,
1305
+ outFile,
1306
+ runInstall,
1307
+ runTest,
1308
+ verbose,
1309
+ scope
1310
+ };
1311
+ if (failOn !== undefined) {
1312
+ baseOpts.failOn = failOn;
1313
+ }
1314
+ return {
1315
+ cmd,
1316
+ opts: baseOpts
1317
+ };
484
1318
  };
485
- var exitCode = (sev) => {
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
+ }
486
1337
  if (sev === "green")
487
1338
  return 0;
488
1339
  if (sev === "yellow")
@@ -496,14 +1347,42 @@ var main = async () => {
496
1347
  `);
497
1348
  process.exit(1);
498
1349
  }
499
- const res = await analyzeRepo(opts);
1350
+ const config = await mergeConfigWithOpts(null, opts);
1351
+ const scanOpts = {
1352
+ repoPath: opts.repoPath,
1353
+ format: opts.format,
1354
+ outFile: opts.outFile,
1355
+ runInstall: opts.runInstall,
1356
+ runTest: opts.runTest,
1357
+ verbose: opts.verbose,
1358
+ scope: opts.scope
1359
+ };
1360
+ if (opts.failOn !== undefined) {
1361
+ scanOpts.failOn = opts.failOn;
1362
+ }
1363
+ const res = await analyzeRepoOverall(scanOpts);
1364
+ if (res.install?.skipReason || res.test?.skipReason) {
1365
+ const skipWarnings = [];
1366
+ if (res.install?.skipReason) {
1367
+ skipWarnings.push(`Install check skipped: ${res.install.skipReason}`);
1368
+ }
1369
+ if (res.test?.skipReason) {
1370
+ skipWarnings.push(`Test run skipped: ${res.test.skipReason}`);
1371
+ }
1372
+ if (skipWarnings.length > 0) {
1373
+ process.stderr.write(`WARNING:
1374
+ ${skipWarnings.map((w) => ` - ${w}`).join(`
1375
+ `)}
1376
+ `);
1377
+ }
1378
+ }
500
1379
  const out = opts.format === "json" ? renderJson(res) : renderMarkdown(res);
501
1380
  const target = opts.outFile ?? (opts.format === "json" ? "bun-ready.json" : "bun-ready.md");
502
- const resolved = path3.resolve(process.cwd(), target);
1381
+ const resolved = path5.resolve(process.cwd(), target);
503
1382
  await fs3.writeFile(resolved, out, "utf8");
504
1383
  process.stdout.write(`Wrote ${opts.format.toUpperCase()} report to ${resolved}
505
1384
  `);
506
- process.exit(exitCode(res.severity));
1385
+ process.exit(exitCode(res.severity, config?.failOn || opts.failOn));
507
1386
  };
508
1387
  main().catch((e) => {
509
1388
  const msg = e instanceof Error ? e.message : String(e);