dependency-radar 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -13,10 +13,19 @@ const importGraphRunner_1 = require("./runners/importGraphRunner");
13
13
  const npmAudit_1 = require("./runners/npmAudit");
14
14
  const npmLs_1 = require("./runners/npmLs");
15
15
  const npmOutdated_1 = require("./runners/npmOutdated");
16
+ const lockfileSignals_1 = require("./runners/lockfileSignals");
16
17
  const report_1 = require("./report");
18
+ const compare_1 = require("./compare");
19
+ const outputFormats_1 = require("./outputFormats");
20
+ const why_1 = require("./why");
21
+ const schema_1 = require("./schema");
17
22
  const failOn_1 = require("./failOn");
18
23
  const promises_1 = __importDefault(require("fs/promises"));
19
24
  const utils_1 = require("./utils");
25
+ const skippedToolResult = {
26
+ ok: true,
27
+ status: "skipped",
28
+ };
20
29
  function normalizeSlashes(p) {
21
30
  return p.split(path_1.default.sep).join("/");
22
31
  }
@@ -425,6 +434,8 @@ function inferPackageManager(rootPkg) {
425
434
  return "pnpm";
426
435
  if (raw.startsWith("yarn@") || raw === "yarn")
427
436
  return "yarn";
437
+ if (raw.startsWith("bun@") || raw === "bun")
438
+ return "bun";
428
439
  if (raw.startsWith("npm@") || raw === "npm")
429
440
  return "npm";
430
441
  return undefined;
@@ -437,6 +448,8 @@ async function detectPackageManager(projectPath, rootPkg, workspaceType) {
437
448
  return workspaceType;
438
449
  if (await detectYarnPnP(projectPath))
439
450
  return "yarn";
451
+ if ((await (0, utils_1.pathExists)(path_1.default.join(projectPath, "bun.lock"))) || (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "bun.lockb"))))
452
+ return "bun";
440
453
  if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules", ".pnpm")))
441
454
  return "pnpm";
442
455
  if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules", ".yarn-state.yml")))
@@ -444,10 +457,14 @@ async function detectPackageManager(projectPath, rootPkg, workspaceType) {
444
457
  return "npm";
445
458
  }
446
459
  async function detectScanManager(projectPath, fallback) {
460
+ if (fallback === "bun" && ((await (0, utils_1.pathExists)(path_1.default.join(projectPath, "bun.lock"))) || (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "bun.lockb")))))
461
+ return "bun";
447
462
  if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "pnpm-lock.yaml")))
448
463
  return "pnpm";
449
464
  if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "yarn.lock")))
450
465
  return "yarn";
466
+ if ((await (0, utils_1.pathExists)(path_1.default.join(projectPath, "bun.lock"))) || (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "bun.lockb"))))
467
+ return "bun";
451
468
  if ((await (0, utils_1.pathExists)(path_1.default.join(projectPath, "package-lock.json"))) ||
452
469
  (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "npm-shrinkwrap.json")))) {
453
470
  return "npm";
@@ -911,11 +928,15 @@ function buildCombinedDependencyGraph(rootPath, packageMetas, dependencyGraphs)
911
928
  return { name: "dependency-radar-workspace", version: "0.0.0", dependencies };
912
929
  }
913
930
  /**
914
- * Parse command-line tokens into a populated CliOptions object.
931
+ * Parse CLI tokens and return a configured CliOptions object reflecting the requested command and flags.
932
+ *
933
+ * Recognizes an optional leading command (scan, explain, compare, why, schema), positional operands for
934
+ * commands that require them (package name for explain/why, compare path for compare), and these flags:
935
+ * --project, --quiet, --out, --keep-temp, --offline, --json, --format, --sbom, --target-node,
936
+ * --audit-signatures, --schema, --timestamp, --open, --no-report, --fail-on, --help / -h.
915
937
  *
916
- * Recognizes a leading non-flag token as the command and the following flags:
917
- * --project, --out, --keep-temp, --offline, --json, --open, --no-report, --fail-on, and --help / -h.
918
- * The --offline flag disables both audit and outdated checks.
938
+ * The --offline flag disables both audit and outdated checks. Unknown options or unexpected positional
939
+ * arguments cause the process to exit with an error.
919
940
  *
920
941
  * @param argv - Array of CLI tokens (typically process.argv.slice(2))
921
942
  * @returns The resolved CliOptions with defaults applied and values overridden by argv
@@ -923,6 +944,7 @@ function buildCombinedDependencyGraph(rootPath, packageMetas, dependencyGraphs)
923
944
  function parseArgs(argv) {
924
945
  const opts = {
925
946
  command: "scan",
947
+ commandProvided: false,
926
948
  project: process.cwd(),
927
949
  quiet: false,
928
950
  out: "dependency-radar.html",
@@ -933,12 +955,18 @@ function parseArgs(argv) {
933
955
  open: false,
934
956
  noReport: false,
935
957
  failOn: new Set(),
958
+ format: "html",
959
+ auditSignatures: false,
960
+ schema: false,
961
+ outProvided: false,
962
+ timestamp: false,
936
963
  };
937
964
  const args = [...argv];
938
965
  if (args[0] && !args[0].startsWith("-")) {
939
966
  const command = args.shift();
940
- if (command === "scan" || command === "explain") {
967
+ if (command === "scan" || command === "explain" || command === "compare" || command === "why" || command === "schema") {
941
968
  opts.command = command;
969
+ opts.commandProvided = true;
942
970
  }
943
971
  else {
944
972
  opts.invalidCommand = command;
@@ -949,23 +977,65 @@ function parseArgs(argv) {
949
977
  const arg = args.shift();
950
978
  if (!arg)
951
979
  break;
952
- if (!arg.startsWith("-") && opts.command === "explain" && !opts.packageName) {
980
+ if (!arg.startsWith("-") && (opts.command === "explain" || opts.command === "why") && !opts.packageName) {
953
981
  opts.packageName = arg;
954
982
  }
955
- else if (arg === "--project" && args[0])
956
- opts.project = args.shift();
983
+ else if (!arg.startsWith("-") && opts.command === "compare" && !opts.comparePath) {
984
+ opts.comparePath = arg;
985
+ }
986
+ else if (!arg.startsWith("-")) {
987
+ console.error(`Unexpected argument: "${arg}".`);
988
+ process.exit(1);
989
+ }
990
+ else if (arg === "--project")
991
+ opts.project = takeOptionValue(args, arg, true);
957
992
  else if (arg === "--quiet")
958
993
  opts.quiet = true;
959
- else if (arg === "--out" && args[0])
960
- opts.out = args.shift();
994
+ else if (arg === "--out") {
995
+ opts.out = takeOptionValue(args, arg, true);
996
+ opts.outProvided = true;
997
+ }
961
998
  else if (arg === "--keep-temp")
962
999
  opts.keepTemp = true;
963
1000
  else if (arg === "--offline") {
964
1001
  opts.audit = false;
965
1002
  opts.outdated = false;
966
1003
  }
967
- else if (arg === "--json")
1004
+ else if (arg === "--json") {
968
1005
  opts.json = true;
1006
+ opts.format = "json";
1007
+ }
1008
+ else if (arg === "--format") {
1009
+ const format = takeOptionValue(args, arg);
1010
+ if (!isReportFormat(format)) {
1011
+ console.error(`Unknown --format: "${format}". Supported formats: html, json, sarif, cyclonedx, spdx.`);
1012
+ process.exit(1);
1013
+ }
1014
+ opts.format = format;
1015
+ opts.json = format === "json";
1016
+ }
1017
+ else if (arg === "--sbom") {
1018
+ const format = takeOptionValue(args, arg);
1019
+ if (format !== "cyclonedx" && format !== "spdx") {
1020
+ console.error('Unknown --sbom format. Supported formats: cyclonedx, spdx.');
1021
+ process.exit(1);
1022
+ }
1023
+ opts.format = format;
1024
+ }
1025
+ else if (arg === "--target-node") {
1026
+ const value = Number.parseInt(takeOptionValue(args, arg), 10);
1027
+ if (!Number.isFinite(value) || value <= 0) {
1028
+ console.error("--target-node must be a positive Node.js major version.");
1029
+ process.exit(1);
1030
+ }
1031
+ opts.targetNodeMajor = value;
1032
+ }
1033
+ else if (arg === "--audit-signatures")
1034
+ opts.auditSignatures = true;
1035
+ else if (arg === "--schema")
1036
+ opts.schema = true;
1037
+ else if (arg === "--timestamp")
1038
+ opts.timestamp = true;
969
1039
  else if (arg === "--open")
970
1040
  opts.open = true;
971
1041
  else if (arg === "--no-report")
@@ -993,35 +1063,128 @@ function parseArgs(argv) {
993
1063
  printHelp();
994
1064
  process.exit(0);
995
1065
  }
1066
+ else {
1067
+ console.error(`Unknown option: "${arg}".`);
1068
+ process.exit(1);
1069
+ }
996
1070
  }
997
1071
  return opts;
998
1072
  }
1073
+ /**
1074
+ * Extracts and returns the next CLI token as the value for a given option, consuming it from `args`.
1075
+ *
1076
+ * @param args - Remaining argv tokens; the first element will be removed and returned.
1077
+ * @param option - The option name shown in the error message when a value is missing.
1078
+ * @param allowLeadingDash - When `true`, permit values that begin with `-`; otherwise treat such tokens as missing values.
1079
+ * @returns The consumed option value.
1080
+ * @remarks Exits the process with code 1 and prints an error if no valid value is present.
1081
+ */
1082
+ function takeOptionValue(args, option, allowLeadingDash = false) {
1083
+ const value = args[0];
1084
+ if (!value || (!allowLeadingDash && value.startsWith("-"))) {
1085
+ console.error(`Missing value for ${option}.`);
1086
+ process.exit(1);
1087
+ }
1088
+ return args.shift();
1089
+ }
1090
+ /**
1091
+ * Checks whether a string is a supported report format.
1092
+ *
1093
+ * @param value - The string to test
1094
+ * @returns `true` if `value` is one of `html`, `json`, `sarif`, `cyclonedx`, or `spdx`, `false` otherwise.
1095
+ */
1096
+ function isReportFormat(value) {
1097
+ return value === "html" || value === "json" || value === "sarif" || value === "cyclonedx" || value === "spdx";
1098
+ }
1099
+ /**
1100
+ * Formats a numeric date/time component as a two-digit string.
1101
+ *
1102
+ * @param value - The numeric part (e.g., day, month, hour, or minute) to format
1103
+ * @returns The value as a two-character string, padded with a leading zero when needed
1104
+ */
1105
+ function padDatePart(value) {
1106
+ return String(value).padStart(2, "0");
1107
+ }
1108
+ /**
1109
+ * Formats a Date into a filesystem-safe timestamp string suitable for filenames.
1110
+ *
1111
+ * @param date - The date to format (local time is used).
1112
+ * @returns A string in the form `YYYY-MM-DD_HH-MM-SS` (zero-padded).
1113
+ */
1114
+ function formatFilenameTimestamp(date) {
1115
+ return [
1116
+ date.getFullYear(),
1117
+ padDatePart(date.getMonth() + 1),
1118
+ padDatePart(date.getDate()),
1119
+ ].join("-") + "_" + [
1120
+ padDatePart(date.getHours()),
1121
+ padDatePart(date.getMinutes()),
1122
+ padDatePart(date.getSeconds()),
1123
+ ].join("-");
1124
+ }
1125
+ /**
1126
+ * Insert a local timestamp into the filename portion of an output path.
1127
+ *
1128
+ * If the path has a file extension, the timestamp is inserted before the extension;
1129
+ * otherwise the timestamp is appended to the filename. The directory portion is preserved.
1130
+ *
1131
+ * @param outputPath - The file path whose filename will receive the timestamp
1132
+ * @param date - Date to derive the timestamp from; defaults to the current date/time
1133
+ * @returns The input path with a timestamp embedded into the filename, preserving directory and extension
1134
+ */
1135
+ function addTimestampToOutputPath(outputPath, date = new Date()) {
1136
+ const parsed = path_1.default.parse(outputPath);
1137
+ const timestamp = formatFilenameTimestamp(date);
1138
+ const basename = parsed.ext
1139
+ ? `${parsed.name}.${timestamp}${parsed.ext}`
1140
+ : `${parsed.name}.${timestamp}`;
1141
+ return path_1.default.join(parsed.dir, basename);
1142
+ }
1143
+ /**
1144
+ * Determines whether the given package manager uses registry-based collectors.
1145
+ *
1146
+ * @returns `true` if the manager is `npm`, `pnpm`, or `yarn`, `false` otherwise.
1147
+ */
1148
+ function supportsRegistryCollectors(manager) {
1149
+ return manager === "npm" || manager === "pnpm" || manager === "yarn";
1150
+ }
999
1151
  /**
1000
1152
  * Print the CLI usage and available options to the console.
1001
1153
  *
1002
1154
  * Displays the command synopsis and descriptions for supported flags including
1003
- * --project, --out, --json, --no-report, --keep-temp, --offline, --open, and --fail-on.
1155
+ * --project, --out, --json, --timestamp, --no-report, --keep-temp, --offline, --open, and --fail-on.
1004
1156
  */
1005
1157
  function printHelp() {
1006
1158
  console.log(`dependency-radar [scan] [options]
1007
1159
  dependency-radar explain <package-name> [options]
1160
+ dependency-radar why <package-name> [options]
1161
+ dependency-radar compare <previous dependency-radar.json> [options]
1008
1162
 
1009
1163
  If no command is provided, \`scan\` is run by default.
1010
1164
 
1011
1165
  Options:
1012
1166
  --project <path> Project folder (default: cwd)
1013
1167
  --quiet Suppress progress/info logs but keep summary and failures
1014
- --out <path> Output HTML file (default: dependency-radar.html)
1168
+ --out <path> Output file (default depends on format)
1169
+ --format <format> Output format: html, json, sarif, cyclonedx, spdx
1170
+ --sbom <format> Write an SBOM: cyclonedx or spdx
1171
+ --target-node <n> Add Node major compatibility findings
1172
+ --audit-signatures Verify npm registry signatures/provenance (opt-in, online only)
1173
+ --schema Print JSON schema, or write it when --out is provided
1015
1174
  --json Write aggregated data to JSON (default filename: dependency-radar.json)
1175
+ --timestamp Add a local timestamp to generated report filenames
1016
1176
  --no-report Do not write HTML/JSON report files or temp artifacts to disk
1017
1177
  --keep-temp Keep .dependency-radar folder
1018
1178
  --offline Skip npm audit and npm outdated (useful for offline scans)
1019
1179
  --open Open the generated report using the system default application
1020
1180
  --fail-on <rules> Fail with exit code 1 when selected rules are violated
1021
1181
  Supported: reachable-vuln, production-vuln, high-severity-vuln,
1022
- licence-mismatch, copyleft-detected, unknown-licence
1182
+ licence-mismatch, copyleft-detected, unknown-licence,
1183
+ supply-chain-source
1023
1184
 
1024
1185
  \`explain\` reuses the same local scan model and prints a terminal view for one package.
1186
+ \`why\` prints shortest dependency paths for one package.
1187
+ \`compare\` scans the current project and compares it with a previous JSON report.
1025
1188
  `);
1026
1189
  }
1027
1190
  /**
@@ -1329,20 +1492,44 @@ function printCliSummary(summary) {
1329
1492
  }
1330
1493
  console.log("");
1331
1494
  }
1495
+ function formatLabel(format) {
1496
+ if (format === "html")
1497
+ return "Report";
1498
+ if (format === "json")
1499
+ return "JSON";
1500
+ if (format === "sarif")
1501
+ return "SARIF";
1502
+ if (format === "cyclonedx")
1503
+ return "CycloneDX SBOM";
1504
+ return "SPDX SBOM";
1505
+ }
1506
+ /**
1507
+ * Run a full dependency analysis for a project/workspace and return the aggregated results.
1508
+ *
1509
+ * Performs workspace detection, per-package collection (audit, dependency trees, import graphs, outdated), merges collected data, runs aggregation and policy evaluation, and optionally writes report artifacts to disk. May create a temporary directory under the project (projectPath/.dependency-radar) and will remove it unless `opts.keepTemp` is set.
1510
+ *
1511
+ * @param opts - CLI-resolved options controlling project path, enabled collectors (audit/outdated), artifact format/output, reporting flags, and policy rules.
1512
+ * @param options.shouldWriteArtifacts - If true, write report artifacts (JSON/HTML/SBOM) to the resolved output path.
1513
+ * @param options.emitArtifactSummary - If true, print a summary line about artifact creation to stdout.
1514
+ * @param options.emitWorkspacePackageSummary - If true, print a brief workspace package summary when a workspace is detected.
1515
+ * @returns An AnalysisExecutionResult containing the aggregated report, CLI summary, policy violations, dependency counts, timing, output path/creation status, collector availability, and workspace metadata.
1516
+ */
1332
1517
  async function executeAnalysis(opts, options) {
1333
- var _a;
1518
+ var _a, _b;
1334
1519
  const shouldWriteArtifacts = options.shouldWriteArtifacts;
1335
1520
  const projectPath = path_1.default.resolve(opts.project);
1336
- let outputPath = path_1.default.resolve(opts.out);
1521
+ let outputPath = opts.outProvided
1522
+ ? path_1.default.resolve(opts.out)
1523
+ : path_1.default.resolve(projectPath, opts.out);
1337
1524
  const startTime = Date.now();
1338
1525
  let dependencyCount = 0;
1339
1526
  let outputCreated = false;
1340
1527
  if (opts.command === "scan" && opts.noReport && opts.keepTemp && !opts.quiet) {
1341
1528
  console.log(statusLine("⚠", "--keep-temp is ignored when --no-report is enabled."));
1342
1529
  }
1343
- if (opts.json && opts.out === "dependency-radar.html") {
1344
- opts.out = "dependency-radar.json";
1345
- outputPath = path_1.default.resolve(opts.out);
1530
+ if (!opts.outProvided && opts.format !== "html") {
1531
+ opts.out = (0, outputFormats_1.defaultOutputName)(opts.format);
1532
+ outputPath = path_1.default.resolve(projectPath, opts.out);
1346
1533
  }
1347
1534
  if (shouldWriteArtifacts) {
1348
1535
  try {
@@ -1352,20 +1539,24 @@ async function executeAnalysis(opts, options) {
1352
1539
  if ((stat && stat.isDirectory()) ||
1353
1540
  endsWithSeparator ||
1354
1541
  (!stat && !hasExtension)) {
1355
- outputPath = path_1.default.join(outputPath, opts.json ? "dependency-radar.json" : "dependency-radar.html");
1542
+ outputPath = path_1.default.join(outputPath, (0, outputFormats_1.defaultOutputName)(opts.format));
1356
1543
  }
1357
1544
  }
1358
1545
  catch {
1359
1546
  // ignore, best-effort path normalization
1360
1547
  }
1548
+ if (opts.timestamp) {
1549
+ outputPath = addTimestampToOutputPath(outputPath);
1550
+ }
1361
1551
  }
1362
1552
  const tempDir = path_1.default.join(projectPath, ".dependency-radar");
1363
1553
  const workspace = await detectWorkspace(projectPath);
1364
1554
  const yarnPnP = await detectYarnPnP(projectPath);
1365
1555
  if (workspace.type === "yarn" && workspace.packagePaths.length === 0) {
1366
- console.error("Yarn Plug'n'Play (nodeLinker: pnp) detected. This is not supported yet.");
1367
- console.error("Switch to nodeLinker: node-modules or run in a non-PnP environment.");
1368
- process.exit(1);
1556
+ if (!opts.quiet) {
1557
+ console.log(statusLine("⚠", "Yarn Plug'n'Play detected; using the root package and yarn.lock where possible."));
1558
+ }
1559
+ workspace.packagePaths = [projectPath];
1369
1560
  }
1370
1561
  const hasProjectNodeModules = await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules"));
1371
1562
  if (!hasProjectNodeModules) {
@@ -1373,9 +1564,9 @@ async function executeAnalysis(opts, options) {
1373
1564
  ? "single project"
1374
1565
  : `${workspace.type.toUpperCase()} workspace`;
1375
1566
  const yarnHint = yarnPnP
1376
- ? " Yarn Plug'n'Play appears enabled; Dependency Radar currently requires node_modules linker."
1567
+ ? " Yarn Plug'n'Play appears enabled; lockfile graph data will be used where possible, but package metadata from zip/cache files is not crawled in this release."
1377
1568
  : "";
1378
- console.warn(colorLeadingSymbol(`⚠ node_modules was not found at ${projectPath}. Scan completeness may be reduced for this ${workspaceHint}. Run your package manager install (npm install, pnpm install, or yarn install) before scanning.${yarnHint}`));
1569
+ console.warn(colorLeadingSymbol(`⚠ node_modules was not found at ${projectPath}. Scan completeness may be reduced for this ${workspaceHint}. Run your package manager install before scanning when local package metadata is required.${yarnHint}`));
1379
1570
  }
1380
1571
  const rootPkg = await readJsonFile(path_1.default.join(projectPath, "package.json"));
1381
1572
  const projectDependencyPolicy = workspace.pnpmWorkspaceOverrides
@@ -1394,20 +1585,22 @@ async function executeAnalysis(opts, options) {
1394
1585
  getToolVersion("pnpm", projectPath),
1395
1586
  getToolVersion("yarn", projectPath),
1396
1587
  ]);
1588
+ const bunVersion = await getToolVersion("bun", projectPath);
1397
1589
  const toolVersions = compactToolVersions({
1398
1590
  npm: npmVersion,
1399
1591
  pnpm: pnpmVersion,
1400
1592
  yarn: yarnVersion,
1593
+ bun: bunVersion,
1401
1594
  });
1402
1595
  const packageManagerVersion = scanManager === "npm"
1403
1596
  ? npmVersion
1404
1597
  : scanManager === "pnpm"
1405
1598
  ? pnpmVersion
1406
- : yarnVersion;
1407
- if (packageManager === "yarn" && yarnPnP) {
1408
- console.error("Yarn Plug'n'Play (nodeLinker: pnp) detected. This is not supported yet.");
1409
- console.error("Switch to nodeLinker: node-modules or run in a non-PnP environment.");
1410
- process.exit(1);
1599
+ : scanManager === "yarn"
1600
+ ? yarnVersion
1601
+ : bunVersion;
1602
+ if (!opts.quiet && packageManager === "yarn" && yarnPnP) {
1603
+ console.log(statusLine("⚠", "Yarn Plug'n'Play detected; using lockfile-derived graph data where available."));
1411
1604
  }
1412
1605
  const packagePaths = workspace.packagePaths;
1413
1606
  const workspaceLabel = workspace.type === "none"
@@ -1430,6 +1623,11 @@ async function executeAnalysis(opts, options) {
1430
1623
  const perPackageLs = [];
1431
1624
  const perPackageImportGraph = [];
1432
1625
  const perPackageOutdated = [];
1626
+ const supplyChainResult = await (0, lockfileSignals_1.runLockfileSupplyChainSignals)(projectPath, tempDir, {
1627
+ persistToDisk: shouldWriteArtifacts,
1628
+ auditSignatures: opts.auditSignatures,
1629
+ offline: !opts.audit
1630
+ }).catch((err) => ({ ok: false, error: String(err) }));
1433
1631
  for (const meta of packageMetas) {
1434
1632
  spinner.update(`Scanning ${workspaceLabel} (${perPackageLs.length + 1}/${packageMetas.length}) at ${projectPath}`);
1435
1633
  const pkgTempDir = path_1.default.join(tempDir, meta.name.replace(/[^a-zA-Z0-9._-]/g, "_"));
@@ -1438,8 +1636,9 @@ async function executeAnalysis(opts, options) {
1438
1636
  }
1439
1637
  const [a, l, ig, o] = await Promise.all([
1440
1638
  opts.audit
1639
+ && supportsRegistryCollectors(scanManager)
1441
1640
  ? (0, npmAudit_1.runPackageAudit)(meta.path, pkgTempDir, scanManager, yarnVersion, { persistToDisk: shouldWriteArtifacts }).catch((err) => ({ ok: false, error: String(err) }))
1442
- : Promise.resolve(undefined),
1641
+ : Promise.resolve(opts.audit ? skippedToolResult : undefined),
1443
1642
  (0, npmLs_1.runNpmLs)(meta.path, pkgTempDir, scanManager, {
1444
1643
  contextLabel: meta.name,
1445
1644
  lockfileSearchRoot: projectPath,
@@ -1450,8 +1649,9 @@ async function executeAnalysis(opts, options) {
1450
1649
  persistToDisk: shouldWriteArtifacts,
1451
1650
  }).catch((err) => ({ ok: false, error: String(err) })),
1452
1651
  opts.outdated
1652
+ && supportsRegistryCollectors(scanManager)
1453
1653
  ? (0, npmOutdated_1.runPackageOutdated)(meta.path, pkgTempDir, scanManager, { persistToDisk: shouldWriteArtifacts }).catch((err) => ({ ok: false, error: String(err) }))
1454
- : Promise.resolve(undefined),
1654
+ : Promise.resolve(opts.outdated ? skippedToolResult : undefined),
1455
1655
  ]);
1456
1656
  perPackageAudit.push(a);
1457
1657
  perPackageLs.push(l);
@@ -1460,14 +1660,25 @@ async function executeAnalysis(opts, options) {
1460
1660
  }
1461
1661
  if (opts.audit) {
1462
1662
  const auditOk = perPackageAudit.every((r) => r && r.ok);
1663
+ const auditSkipped = perPackageAudit.every((r) => (r === null || r === void 0 ? void 0 : r.status) === "skipped");
1463
1664
  if (!opts.quiet || !auditOk) {
1464
- spinner.log(statusLine(auditOk ? "✔" : "✖", `${scanManager.toUpperCase()} audit data ${auditOk ? "collected" : "unavailable"}`));
1665
+ spinner.log(statusLine(auditSkipped ? "ℹ" : auditOk ? "✔" : "✖", `${scanManager.toUpperCase()} audit data ${auditSkipped ? "skipped" : auditOk ? "collected" : "unavailable"}`));
1666
+ }
1667
+ }
1668
+ if (opts.auditSignatures && !opts.quiet) {
1669
+ const audit = supplyChainResult.ok ? (_a = supplyChainResult.data) === null || _a === void 0 ? void 0 : _a.signatureAudit : undefined;
1670
+ if ((audit === null || audit === void 0 ? void 0 : audit.status) === "skipped") {
1671
+ spinner.log(statusLine("⚠", "npm audit signatures skipped (--offline)"));
1672
+ }
1673
+ else {
1674
+ spinner.log(statusLine((audit === null || audit === void 0 ? void 0 : audit.ok) ? "✔" : "✖", `npm audit signatures ${(audit === null || audit === void 0 ? void 0 : audit.ok) ? "verified" : "unavailable"}`));
1465
1675
  }
1466
1676
  }
1467
1677
  if (opts.outdated) {
1468
1678
  const outdatedOk = perPackageOutdated.every((r) => r.result && r.result.ok);
1679
+ const outdatedSkipped = perPackageOutdated.every((r) => { var _a; return ((_a = r.result) === null || _a === void 0 ? void 0 : _a.status) === "skipped"; });
1469
1680
  if (!opts.quiet || !outdatedOk) {
1470
- spinner.log(statusLine(outdatedOk ? "✔" : "✖", `${scanManager.toUpperCase()} outdated data ${outdatedOk ? "collected" : "unavailable"}`));
1681
+ spinner.log(statusLine(outdatedSkipped ? "ℹ" : outdatedOk ? "✔" : "✖", `${scanManager.toUpperCase()} outdated data ${outdatedSkipped ? "skipped" : outdatedOk ? "collected" : "unavailable"}`));
1471
1682
  }
1472
1683
  }
1473
1684
  const mergedAuditData = mergeAuditResults(perPackageAudit.map((r) => (r && r.ok ? r.data : undefined)));
@@ -1498,7 +1709,7 @@ async function executeAnalysis(opts, options) {
1498
1709
  if (lsFailures.length > 0) {
1499
1710
  const packageList = lsFailures.map((entry) => { var _a; return (_a = entry.meta) === null || _a === void 0 ? void 0 : _a.name; }).filter(Boolean);
1500
1711
  spinner.log(`Dependency tree warning: ${lsFailures.length} package${lsFailures.length === 1 ? "" : "s"} failed (${packageList.join(", ")}).`);
1501
- spinner.log(`First dependency tree error: ${((_a = lsFailures[0].result) === null || _a === void 0 ? void 0 : _a.error) || "pnpm ls failed"}`);
1712
+ spinner.log(`First dependency tree error: ${((_b = lsFailures[0].result) === null || _b === void 0 ? void 0 : _b.error) || "pnpm ls failed"}`);
1502
1713
  }
1503
1714
  if (importFailures.length > 0) {
1504
1715
  spinner.log(`Import graph warning: ${importFailures.length} package${importFailures.length === 1 ? "" : "s"} failed (${importFailures[0].error || "import graph failed"})`);
@@ -1509,6 +1720,7 @@ async function executeAnalysis(opts, options) {
1509
1720
  npmLsResult,
1510
1721
  importGraphResult,
1511
1722
  outdatedResult,
1723
+ supplyChainResult,
1512
1724
  pkgOverride: mergedPkgForAggregator,
1513
1725
  projectPackageJson: rootPkg,
1514
1726
  ...(projectDependencyPolicy ? { projectDependencyPolicy } : {}),
@@ -1536,6 +1748,7 @@ async function executeAnalysis(opts, options) {
1536
1748
  arch: process.arch,
1537
1749
  ci: isCI(),
1538
1750
  ...(toolVersions ? { toolVersions } : {}),
1751
+ ...(typeof opts.targetNodeMajor === "number" ? { targetNodeMajor: opts.targetNodeMajor } : {}),
1539
1752
  });
1540
1753
  dependencyCount = Object.keys(aggregated.dependencies).length;
1541
1754
  const importGraphComplete = perPackageImportGraph.every((result) => result.ok);
@@ -1547,10 +1760,22 @@ async function executeAnalysis(opts, options) {
1547
1760
  console.log(`Detected ${workspace.type.toUpperCase()} workspace with ${packagePaths.length} package${packagePaths.length === 1 ? "" : "s"}.`);
1548
1761
  }
1549
1762
  if (dependencyCount > 0 && shouldWriteArtifacts) {
1550
- if (opts.json) {
1763
+ if (opts.format === "json") {
1551
1764
  await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
1552
1765
  await promises_1.default.writeFile(outputPath, JSON.stringify(aggregated, null, 2), "utf8");
1553
1766
  }
1767
+ else if (opts.format === "sarif") {
1768
+ await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
1769
+ await promises_1.default.writeFile(outputPath, (0, outputFormats_1.renderSarif)(aggregated), "utf8");
1770
+ }
1771
+ else if (opts.format === "cyclonedx") {
1772
+ await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
1773
+ await promises_1.default.writeFile(outputPath, (0, outputFormats_1.renderCycloneDx)(aggregated), "utf8");
1774
+ }
1775
+ else if (opts.format === "spdx") {
1776
+ await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
1777
+ await promises_1.default.writeFile(outputPath, (0, outputFormats_1.renderSpdx)(aggregated), "utf8");
1778
+ }
1554
1779
  else {
1555
1780
  await (0, report_1.renderReport)(aggregated, outputPath);
1556
1781
  }
@@ -1566,10 +1791,10 @@ async function executeAnalysis(opts, options) {
1566
1791
  console.log(statusLine("ℹ", "Report output disabled (--no-report); no report artifacts written."));
1567
1792
  }
1568
1793
  else if (outputCreated) {
1569
- console.log(statusLine("✔", `${opts.json ? "JSON" : "Report"} written to ${outputPath}`));
1794
+ console.log(statusLine("✔", `${formatLabel(opts.format)} written to ${outputPath}`));
1570
1795
  }
1571
1796
  else {
1572
- console.log(statusLine("✖", `No dependencies were found - ${opts.json ? "JSON file" : "Report"} not created`));
1797
+ console.log(statusLine("✖", `No dependencies were found - ${formatLabel(opts.format)} not created`));
1573
1798
  }
1574
1799
  }
1575
1800
  return {
@@ -1619,7 +1844,7 @@ async function runScanCommand(opts) {
1619
1844
  console.log(statusLine("✖", "Skipping auto-open because --no-report is enabled."));
1620
1845
  }
1621
1846
  else if (opts.open && result.outputCreated && !isCI()) {
1622
- console.log(statusLine("↗", `Opening ${path_1.default.basename(result.outputPath)} using system default ${opts.json ? "application" : "browser"}.`));
1847
+ console.log(statusLine("↗", `Opening ${path_1.default.basename(result.outputPath)} using system default ${opts.format === "html" ? "browser" : "application"}.`));
1623
1848
  openInBrowser(result.outputPath);
1624
1849
  }
1625
1850
  else if (opts.open && result.outputCreated && isCI()) {
@@ -1658,6 +1883,64 @@ async function runExplainCommand(opts) {
1658
1883
  process.exit(1);
1659
1884
  }
1660
1885
  }
1886
+ async function runWhyCommand(opts) {
1887
+ var _a;
1888
+ const packageName = (_a = opts.packageName) === null || _a === void 0 ? void 0 : _a.trim();
1889
+ if (!packageName) {
1890
+ console.error("Missing package name for why. Usage: dependency-radar why <package-name>");
1891
+ process.exit(1);
1892
+ return;
1893
+ }
1894
+ const result = await executeAnalysis(opts, {
1895
+ shouldWriteArtifacts: false,
1896
+ emitArtifactSummary: false,
1897
+ emitWorkspacePackageSummary: false,
1898
+ });
1899
+ console.log("");
1900
+ const output = (0, why_1.formatWhyOutput)(result.aggregated, packageName);
1901
+ console.log(output);
1902
+ if (output.startsWith("Package not found")) {
1903
+ process.exit(1);
1904
+ }
1905
+ }
1906
+ async function runCompareCommand(opts) {
1907
+ var _a;
1908
+ const previousPath = (_a = opts.comparePath) === null || _a === void 0 ? void 0 : _a.trim();
1909
+ if (!previousPath) {
1910
+ console.error("Missing previous report path. Usage: dependency-radar compare <previous dependency-radar.json>");
1911
+ process.exit(1);
1912
+ return;
1913
+ }
1914
+ let previous;
1915
+ try {
1916
+ const parsed = JSON.parse(await promises_1.default.readFile(path_1.default.resolve(previousPath), "utf8"));
1917
+ const schemaVersion = parsed && typeof parsed === "object" ? parsed.schemaVersion : undefined;
1918
+ if (!parsed ||
1919
+ typeof parsed !== "object" ||
1920
+ schemaVersion !== schema_1.REPORT_SCHEMA_VERSION ||
1921
+ !parsed.project ||
1922
+ !parsed.summary ||
1923
+ !parsed.dependencies ||
1924
+ typeof parsed.dependencies !== "object") {
1925
+ console.error(`Previous report schema mismatch: expected schemaVersion ${schema_1.REPORT_SCHEMA_VERSION}, found ${schemaVersion !== null && schemaVersion !== void 0 ? schemaVersion : "missing"}.`);
1926
+ process.exit(1);
1927
+ return;
1928
+ }
1929
+ previous = parsed;
1930
+ }
1931
+ catch (err) {
1932
+ console.error(`Could not read previous report at ${previousPath}: ${err instanceof Error ? err.message : String(err)}`);
1933
+ process.exit(1);
1934
+ return;
1935
+ }
1936
+ const result = await executeAnalysis(opts, {
1937
+ shouldWriteArtifacts: false,
1938
+ emitArtifactSummary: false,
1939
+ emitWorkspacePackageSummary: false,
1940
+ });
1941
+ console.log("");
1942
+ console.log((0, compare_1.formatCompareOutput)((0, compare_1.compareReports)(previous, result.aggregated)));
1943
+ }
1661
1944
  /**
1662
1945
  * Run the CLI entrypoint and dispatch to the selected command.
1663
1946
  */
@@ -1669,10 +1952,22 @@ async function run() {
1669
1952
  return;
1670
1953
  }
1671
1954
  try {
1955
+ if ((opts.schema && !opts.commandProvided) || opts.command === "schema") {
1956
+ await runSchemaCommand(opts);
1957
+ return;
1958
+ }
1672
1959
  if (opts.command === "explain") {
1673
1960
  await runExplainCommand(opts);
1674
1961
  return;
1675
1962
  }
1963
+ if (opts.command === "why") {
1964
+ await runWhyCommand(opts);
1965
+ return;
1966
+ }
1967
+ if (opts.command === "compare") {
1968
+ await runCompareCommand(opts);
1969
+ return;
1970
+ }
1676
1971
  await runScanCommand(opts);
1677
1972
  }
1678
1973
  catch (err) {
@@ -1680,6 +1975,19 @@ async function run() {
1680
1975
  process.exit(1);
1681
1976
  }
1682
1977
  }
1978
+ async function runSchemaCommand(opts) {
1979
+ const schema = (0, schema_1.renderReportJsonSchema)();
1980
+ if (opts.outProvided) {
1981
+ const outputPath = path_1.default.resolve(opts.out);
1982
+ await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
1983
+ await promises_1.default.writeFile(outputPath, schema, "utf8");
1984
+ if (!opts.quiet) {
1985
+ console.log(statusLine("✔", `JSON schema written to ${outputPath}`));
1986
+ }
1987
+ return;
1988
+ }
1989
+ console.log(schema);
1990
+ }
1683
1991
  run();
1684
1992
  function createProgressReporter(text, quiet) {
1685
1993
  if (!quiet)