dependency-radar 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +189 -25
- package/dist/aggregator.js +35 -9
- package/dist/cli.js +501 -103
- package/dist/compare.js +79 -0
- package/dist/explain.js +193 -0
- package/dist/failOn.js +16 -2
- package/dist/findings.js +166 -0
- package/dist/generated/spdx.js +3 -0
- package/dist/nodeEngine.js +181 -0
- package/dist/outputFormats.js +185 -0
- package/dist/report-assets.js +2 -2
- package/dist/report.js +138 -72
- package/dist/runners/importGraphRunner.js +9 -5
- package/dist/runners/lockfileGraph.js +144 -1
- package/dist/runners/lockfileSignals.js +303 -0
- package/dist/runners/npmLs.js +15 -0
- package/dist/schema.js +107 -0
- package/dist/utils.js +62 -3
- package/dist/why.js +69 -0
- package/dist/workspaceFilter.js +25 -0
- package/package.json +8 -5
- package/dist/runners/depcheckRunner.js +0 -23
- package/dist/runners/licenseChecker.js +0 -33
- package/dist/runners/madgeRunner.js +0 -29
package/dist/cli.js
CHANGED
|
@@ -8,14 +8,24 @@ const path_1 = __importDefault(require("path"));
|
|
|
8
8
|
const child_process_1 = require("child_process");
|
|
9
9
|
const os_1 = require("os");
|
|
10
10
|
const aggregator_1 = require("./aggregator");
|
|
11
|
+
const explain_1 = require("./explain");
|
|
11
12
|
const importGraphRunner_1 = require("./runners/importGraphRunner");
|
|
12
13
|
const npmAudit_1 = require("./runners/npmAudit");
|
|
13
14
|
const npmLs_1 = require("./runners/npmLs");
|
|
14
15
|
const npmOutdated_1 = require("./runners/npmOutdated");
|
|
16
|
+
const lockfileSignals_1 = require("./runners/lockfileSignals");
|
|
15
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");
|
|
16
22
|
const failOn_1 = require("./failOn");
|
|
17
23
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
18
24
|
const utils_1 = require("./utils");
|
|
25
|
+
const skippedToolResult = {
|
|
26
|
+
ok: true,
|
|
27
|
+
status: "skipped",
|
|
28
|
+
};
|
|
19
29
|
function normalizeSlashes(p) {
|
|
20
30
|
return p.split(path_1.default.sep).join("/");
|
|
21
31
|
}
|
|
@@ -424,6 +434,8 @@ function inferPackageManager(rootPkg) {
|
|
|
424
434
|
return "pnpm";
|
|
425
435
|
if (raw.startsWith("yarn@") || raw === "yarn")
|
|
426
436
|
return "yarn";
|
|
437
|
+
if (raw.startsWith("bun@") || raw === "bun")
|
|
438
|
+
return "bun";
|
|
427
439
|
if (raw.startsWith("npm@") || raw === "npm")
|
|
428
440
|
return "npm";
|
|
429
441
|
return undefined;
|
|
@@ -436,6 +448,8 @@ async function detectPackageManager(projectPath, rootPkg, workspaceType) {
|
|
|
436
448
|
return workspaceType;
|
|
437
449
|
if (await detectYarnPnP(projectPath))
|
|
438
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";
|
|
439
453
|
if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules", ".pnpm")))
|
|
440
454
|
return "pnpm";
|
|
441
455
|
if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules", ".yarn-state.yml")))
|
|
@@ -443,10 +457,14 @@ async function detectPackageManager(projectPath, rootPkg, workspaceType) {
|
|
|
443
457
|
return "npm";
|
|
444
458
|
}
|
|
445
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";
|
|
446
462
|
if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "pnpm-lock.yaml")))
|
|
447
463
|
return "pnpm";
|
|
448
464
|
if (await (0, utils_1.pathExists)(path_1.default.join(projectPath, "yarn.lock")))
|
|
449
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";
|
|
450
468
|
if ((await (0, utils_1.pathExists)(path_1.default.join(projectPath, "package-lock.json"))) ||
|
|
451
469
|
(await (0, utils_1.pathExists)(path_1.default.join(projectPath, "npm-shrinkwrap.json")))) {
|
|
452
470
|
return "npm";
|
|
@@ -910,11 +928,15 @@ function buildCombinedDependencyGraph(rootPath, packageMetas, dependencyGraphs)
|
|
|
910
928
|
return { name: "dependency-radar-workspace", version: "0.0.0", dependencies };
|
|
911
929
|
}
|
|
912
930
|
/**
|
|
913
|
-
* Parse
|
|
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.
|
|
914
937
|
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
* 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.
|
|
918
940
|
*
|
|
919
941
|
* @param argv - Array of CLI tokens (typically process.argv.slice(2))
|
|
920
942
|
* @returns The resolved CliOptions with defaults applied and values overridden by argv
|
|
@@ -922,7 +944,9 @@ function buildCombinedDependencyGraph(rootPath, packageMetas, dependencyGraphs)
|
|
|
922
944
|
function parseArgs(argv) {
|
|
923
945
|
const opts = {
|
|
924
946
|
command: "scan",
|
|
947
|
+
commandProvided: false,
|
|
925
948
|
project: process.cwd(),
|
|
949
|
+
quiet: false,
|
|
926
950
|
out: "dependency-radar.html",
|
|
927
951
|
keepTemp: false,
|
|
928
952
|
audit: true,
|
|
@@ -931,27 +955,87 @@ function parseArgs(argv) {
|
|
|
931
955
|
open: false,
|
|
932
956
|
noReport: false,
|
|
933
957
|
failOn: new Set(),
|
|
958
|
+
format: "html",
|
|
959
|
+
auditSignatures: false,
|
|
960
|
+
schema: false,
|
|
961
|
+
outProvided: false,
|
|
962
|
+
timestamp: false,
|
|
934
963
|
};
|
|
935
964
|
const args = [...argv];
|
|
936
965
|
if (args[0] && !args[0].startsWith("-")) {
|
|
937
|
-
|
|
966
|
+
const command = args.shift();
|
|
967
|
+
if (command === "scan" || command === "explain" || command === "compare" || command === "why" || command === "schema") {
|
|
968
|
+
opts.command = command;
|
|
969
|
+
opts.commandProvided = true;
|
|
970
|
+
}
|
|
971
|
+
else {
|
|
972
|
+
opts.invalidCommand = command;
|
|
973
|
+
return opts;
|
|
974
|
+
}
|
|
938
975
|
}
|
|
939
976
|
while (args.length) {
|
|
940
977
|
const arg = args.shift();
|
|
941
978
|
if (!arg)
|
|
942
979
|
break;
|
|
943
|
-
if (arg === "
|
|
944
|
-
opts.
|
|
945
|
-
|
|
946
|
-
|
|
980
|
+
if (!arg.startsWith("-") && (opts.command === "explain" || opts.command === "why") && !opts.packageName) {
|
|
981
|
+
opts.packageName = arg;
|
|
982
|
+
}
|
|
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);
|
|
992
|
+
else if (arg === "--quiet")
|
|
993
|
+
opts.quiet = true;
|
|
994
|
+
else if (arg === "--out") {
|
|
995
|
+
opts.out = takeOptionValue(args, arg, true);
|
|
996
|
+
opts.outProvided = true;
|
|
997
|
+
}
|
|
947
998
|
else if (arg === "--keep-temp")
|
|
948
999
|
opts.keepTemp = true;
|
|
949
1000
|
else if (arg === "--offline") {
|
|
950
1001
|
opts.audit = false;
|
|
951
1002
|
opts.outdated = false;
|
|
952
1003
|
}
|
|
953
|
-
else if (arg === "--json")
|
|
1004
|
+
else if (arg === "--json") {
|
|
954
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;
|
|
955
1039
|
else if (arg === "--open")
|
|
956
1040
|
opts.open = true;
|
|
957
1041
|
else if (arg === "--no-report")
|
|
@@ -979,31 +1063,128 @@ function parseArgs(argv) {
|
|
|
979
1063
|
printHelp();
|
|
980
1064
|
process.exit(0);
|
|
981
1065
|
}
|
|
1066
|
+
else {
|
|
1067
|
+
console.error(`Unknown option: "${arg}".`);
|
|
1068
|
+
process.exit(1);
|
|
1069
|
+
}
|
|
982
1070
|
}
|
|
983
1071
|
return opts;
|
|
984
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
|
+
}
|
|
985
1151
|
/**
|
|
986
1152
|
* Print the CLI usage and available options to the console.
|
|
987
1153
|
*
|
|
988
1154
|
* Displays the command synopsis and descriptions for supported flags including
|
|
989
|
-
* --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.
|
|
990
1156
|
*/
|
|
991
1157
|
function printHelp() {
|
|
992
1158
|
console.log(`dependency-radar [scan] [options]
|
|
1159
|
+
dependency-radar explain <package-name> [options]
|
|
1160
|
+
dependency-radar why <package-name> [options]
|
|
1161
|
+
dependency-radar compare <previous dependency-radar.json> [options]
|
|
993
1162
|
|
|
994
1163
|
If no command is provided, \`scan\` is run by default.
|
|
995
1164
|
|
|
996
1165
|
Options:
|
|
997
1166
|
--project <path> Project folder (default: cwd)
|
|
998
|
-
--
|
|
1167
|
+
--quiet Suppress progress/info logs but keep summary and failures
|
|
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
|
|
999
1174
|
--json Write aggregated data to JSON (default filename: dependency-radar.json)
|
|
1175
|
+
--timestamp Add a local timestamp to generated report filenames
|
|
1000
1176
|
--no-report Do not write HTML/JSON report files or temp artifacts to disk
|
|
1001
1177
|
--keep-temp Keep .dependency-radar folder
|
|
1002
1178
|
--offline Skip npm audit and npm outdated (useful for offline scans)
|
|
1003
1179
|
--open Open the generated report using the system default application
|
|
1004
1180
|
--fail-on <rules> Fail with exit code 1 when selected rules are violated
|
|
1005
1181
|
Supported: reachable-vuln, production-vuln, high-severity-vuln,
|
|
1006
|
-
licence-mismatch, copyleft-detected, unknown-licence
|
|
1182
|
+
licence-mismatch, copyleft-detected, unknown-licence,
|
|
1183
|
+
supply-chain-source
|
|
1184
|
+
|
|
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.
|
|
1007
1188
|
`);
|
|
1008
1189
|
}
|
|
1009
1190
|
/**
|
|
@@ -1070,6 +1251,20 @@ function shouldUseColor() {
|
|
|
1070
1251
|
return Boolean(process.stdout.isTTY);
|
|
1071
1252
|
}
|
|
1072
1253
|
const COLOR_ENABLED = shouldUseColor();
|
|
1254
|
+
function supportsTerminalHyperlinks() {
|
|
1255
|
+
if (!process.stdout.isTTY)
|
|
1256
|
+
return false;
|
|
1257
|
+
if (process.env.NO_COLOR !== undefined)
|
|
1258
|
+
return false;
|
|
1259
|
+
if (process.env.TERM === "dumb")
|
|
1260
|
+
return false;
|
|
1261
|
+
return true;
|
|
1262
|
+
}
|
|
1263
|
+
function formatTerminalLink(label, url) {
|
|
1264
|
+
if (!supportsTerminalHyperlinks())
|
|
1265
|
+
return label;
|
|
1266
|
+
return `\u001B]8;;${url}\u0007${label}\u001B]8;;\u0007`;
|
|
1267
|
+
}
|
|
1073
1268
|
/**
|
|
1074
1269
|
* Wraps text with ANSI color or style escape sequences when terminal coloring is enabled.
|
|
1075
1270
|
*
|
|
@@ -1270,10 +1465,10 @@ function printCliSummary(summary) {
|
|
|
1270
1465
|
const bullet = "•";
|
|
1271
1466
|
console.log("");
|
|
1272
1467
|
console.log("Summary:");
|
|
1273
|
-
console.log(`${bullet} Direct
|
|
1274
|
-
console.log(`${bullet} Transitive
|
|
1468
|
+
console.log(`${bullet} Direct dependencies scanned: ${summary.directDeps}`);
|
|
1469
|
+
console.log(`${bullet} Transitive dependencies scanned: ${summary.transitiveDeps}`);
|
|
1275
1470
|
console.log(`${bullet} Vulnerable packages: ${summary.vulnerablePackages} (${summary.reachableVulnerablePackages} reachable)`);
|
|
1276
|
-
console.log(`${bullet}
|
|
1471
|
+
console.log(`${bullet} Dependencies with no static import reference: ${summary.unusedInstalledDeps}`);
|
|
1277
1472
|
console.log(`${bullet} License mismatches: ${summary.licenseMismatches}`);
|
|
1278
1473
|
console.log(`${bullet} Major upgrade blockers: ${summary.majorUpgradeBlockers}`);
|
|
1279
1474
|
const blockerDetails = [];
|
|
@@ -1297,36 +1492,45 @@ function printCliSummary(summary) {
|
|
|
1297
1492
|
}
|
|
1298
1493
|
console.log("");
|
|
1299
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
|
+
}
|
|
1300
1506
|
/**
|
|
1301
|
-
* Run
|
|
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.
|
|
1302
1510
|
*
|
|
1303
|
-
*
|
|
1304
|
-
*
|
|
1305
|
-
*
|
|
1306
|
-
*
|
|
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.
|
|
1307
1516
|
*/
|
|
1308
|
-
async function
|
|
1309
|
-
var _a;
|
|
1310
|
-
const
|
|
1311
|
-
if (opts.command !== "scan") {
|
|
1312
|
-
printHelp();
|
|
1313
|
-
process.exit(1);
|
|
1314
|
-
return;
|
|
1315
|
-
}
|
|
1316
|
-
const shouldWriteArtifacts = !opts.noReport;
|
|
1517
|
+
async function executeAnalysis(opts, options) {
|
|
1518
|
+
var _a, _b;
|
|
1519
|
+
const shouldWriteArtifacts = options.shouldWriteArtifacts;
|
|
1317
1520
|
const projectPath = path_1.default.resolve(opts.project);
|
|
1318
|
-
let
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
console.log(statusLine("⚠", "--keep-temp is ignored when --no-report is enabled."));
|
|
1322
|
-
}
|
|
1323
|
-
if (opts.json && opts.out === "dependency-radar.html") {
|
|
1324
|
-
opts.out = "dependency-radar.json";
|
|
1325
|
-
}
|
|
1326
|
-
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);
|
|
1327
1524
|
const startTime = Date.now();
|
|
1328
1525
|
let dependencyCount = 0;
|
|
1329
1526
|
let outputCreated = false;
|
|
1527
|
+
if (opts.command === "scan" && opts.noReport && opts.keepTemp && !opts.quiet) {
|
|
1528
|
+
console.log(statusLine("⚠", "--keep-temp is ignored when --no-report is enabled."));
|
|
1529
|
+
}
|
|
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);
|
|
1533
|
+
}
|
|
1330
1534
|
if (shouldWriteArtifacts) {
|
|
1331
1535
|
try {
|
|
1332
1536
|
const stat = await promises_1.default.stat(outputPath).catch(() => undefined);
|
|
@@ -1335,22 +1539,24 @@ async function run() {
|
|
|
1335
1539
|
if ((stat && stat.isDirectory()) ||
|
|
1336
1540
|
endsWithSeparator ||
|
|
1337
1541
|
(!stat && !hasExtension)) {
|
|
1338
|
-
outputPath = path_1.default.join(outputPath,
|
|
1542
|
+
outputPath = path_1.default.join(outputPath, (0, outputFormats_1.defaultOutputName)(opts.format));
|
|
1339
1543
|
}
|
|
1340
1544
|
}
|
|
1341
|
-
catch
|
|
1545
|
+
catch {
|
|
1342
1546
|
// ignore, best-effort path normalization
|
|
1343
1547
|
}
|
|
1548
|
+
if (opts.timestamp) {
|
|
1549
|
+
outputPath = addTimestampToOutputPath(outputPath);
|
|
1550
|
+
}
|
|
1344
1551
|
}
|
|
1345
1552
|
const tempDir = path_1.default.join(projectPath, ".dependency-radar");
|
|
1346
|
-
// Stage 1: detect workspace/package-manager context and collect tool versions.
|
|
1347
1553
|
const workspace = await detectWorkspace(projectPath);
|
|
1348
1554
|
const yarnPnP = await detectYarnPnP(projectPath);
|
|
1349
1555
|
if (workspace.type === "yarn" && workspace.packagePaths.length === 0) {
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
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];
|
|
1354
1560
|
}
|
|
1355
1561
|
const hasProjectNodeModules = await (0, utils_1.pathExists)(path_1.default.join(projectPath, "node_modules"));
|
|
1356
1562
|
if (!hasProjectNodeModules) {
|
|
@@ -1358,9 +1564,9 @@ async function run() {
|
|
|
1358
1564
|
? "single project"
|
|
1359
1565
|
: `${workspace.type.toUpperCase()} workspace`;
|
|
1360
1566
|
const yarnHint = yarnPnP
|
|
1361
|
-
? " Yarn Plug'n'Play appears enabled;
|
|
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."
|
|
1362
1568
|
: "";
|
|
1363
|
-
console.warn(colorLeadingSymbol(`⚠ node_modules was not found at ${projectPath}. Scan completeness may be reduced for this ${workspaceHint}. Run your package manager install
|
|
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}`));
|
|
1364
1570
|
}
|
|
1365
1571
|
const rootPkg = await readJsonFile(path_1.default.join(projectPath, "package.json"));
|
|
1366
1572
|
const projectDependencyPolicy = workspace.pnpmWorkspaceOverrides
|
|
@@ -1379,42 +1585,49 @@ async function run() {
|
|
|
1379
1585
|
getToolVersion("pnpm", projectPath),
|
|
1380
1586
|
getToolVersion("yarn", projectPath),
|
|
1381
1587
|
]);
|
|
1588
|
+
const bunVersion = await getToolVersion("bun", projectPath);
|
|
1382
1589
|
const toolVersions = compactToolVersions({
|
|
1383
1590
|
npm: npmVersion,
|
|
1384
1591
|
pnpm: pnpmVersion,
|
|
1385
1592
|
yarn: yarnVersion,
|
|
1593
|
+
bun: bunVersion,
|
|
1386
1594
|
});
|
|
1387
1595
|
const packageManagerVersion = scanManager === "npm"
|
|
1388
1596
|
? npmVersion
|
|
1389
1597
|
: scanManager === "pnpm"
|
|
1390
1598
|
? pnpmVersion
|
|
1391
|
-
:
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
return;
|
|
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."));
|
|
1397
1604
|
}
|
|
1398
1605
|
const packagePaths = workspace.packagePaths;
|
|
1399
1606
|
const workspaceLabel = workspace.type === "none"
|
|
1400
1607
|
? "Single project"
|
|
1401
1608
|
: `${workspace.type.toUpperCase()} workspace`;
|
|
1402
|
-
|
|
1403
|
-
|
|
1609
|
+
if (!opts.quiet) {
|
|
1610
|
+
console.log(statusLine("✔", `${workspaceLabel} detected`));
|
|
1611
|
+
}
|
|
1612
|
+
if (!opts.quiet && workspace.type !== "none" && scanManager !== workspace.type) {
|
|
1404
1613
|
console.log(statusLine("✔", `Using ${scanManager.toUpperCase()} for dependency data (lockfile detected)`));
|
|
1405
1614
|
}
|
|
1406
|
-
const spinner =
|
|
1615
|
+
const spinner = createProgressReporter(`Scanning ${workspaceLabel} at ${projectPath}`, opts.quiet);
|
|
1407
1616
|
try {
|
|
1408
1617
|
if (shouldWriteArtifacts) {
|
|
1409
1618
|
await (0, utils_1.ensureDir)(tempDir);
|
|
1410
1619
|
}
|
|
1411
|
-
// Stage 2: run per-package collectors and persist raw tool outputs.
|
|
1412
1620
|
const packageMetas = await readWorkspacePackageMeta(projectPath, packagePaths);
|
|
1413
1621
|
const workspaceClassification = buildWorkspaceClassification(projectPath, packageMetas);
|
|
1414
1622
|
const perPackageAudit = [];
|
|
1415
1623
|
const perPackageLs = [];
|
|
1416
1624
|
const perPackageImportGraph = [];
|
|
1417
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) }));
|
|
1418
1631
|
for (const meta of packageMetas) {
|
|
1419
1632
|
spinner.update(`Scanning ${workspaceLabel} (${perPackageLs.length + 1}/${packageMetas.length}) at ${projectPath}`);
|
|
1420
1633
|
const pkgTempDir = path_1.default.join(tempDir, meta.name.replace(/[^a-zA-Z0-9._-]/g, "_"));
|
|
@@ -1423,8 +1636,9 @@ async function run() {
|
|
|
1423
1636
|
}
|
|
1424
1637
|
const [a, l, ig, o] = await Promise.all([
|
|
1425
1638
|
opts.audit
|
|
1639
|
+
&& supportsRegistryCollectors(scanManager)
|
|
1426
1640
|
? (0, npmAudit_1.runPackageAudit)(meta.path, pkgTempDir, scanManager, yarnVersion, { persistToDisk: shouldWriteArtifacts }).catch((err) => ({ ok: false, error: String(err) }))
|
|
1427
|
-
: Promise.resolve(undefined),
|
|
1641
|
+
: Promise.resolve(opts.audit ? skippedToolResult : undefined),
|
|
1428
1642
|
(0, npmLs_1.runNpmLs)(meta.path, pkgTempDir, scanManager, {
|
|
1429
1643
|
contextLabel: meta.name,
|
|
1430
1644
|
lockfileSearchRoot: projectPath,
|
|
@@ -1435,31 +1649,36 @@ async function run() {
|
|
|
1435
1649
|
persistToDisk: shouldWriteArtifacts,
|
|
1436
1650
|
}).catch((err) => ({ ok: false, error: String(err) })),
|
|
1437
1651
|
opts.outdated
|
|
1652
|
+
&& supportsRegistryCollectors(scanManager)
|
|
1438
1653
|
? (0, npmOutdated_1.runPackageOutdated)(meta.path, pkgTempDir, scanManager, { persistToDisk: shouldWriteArtifacts }).catch((err) => ({ ok: false, error: String(err) }))
|
|
1439
|
-
: Promise.resolve(undefined),
|
|
1654
|
+
: Promise.resolve(opts.outdated ? skippedToolResult : undefined),
|
|
1440
1655
|
]);
|
|
1441
1656
|
perPackageAudit.push(a);
|
|
1442
1657
|
perPackageLs.push(l);
|
|
1443
1658
|
perPackageImportGraph.push(ig);
|
|
1444
1659
|
perPackageOutdated.push({ attempted: Boolean(opts.outdated), result: o });
|
|
1445
1660
|
}
|
|
1446
|
-
// Stage 3: merge per-package results into a workspace-level view.
|
|
1447
1661
|
if (opts.audit) {
|
|
1448
1662
|
const auditOk = perPackageAudit.every((r) => r && r.ok);
|
|
1449
|
-
|
|
1450
|
-
|
|
1663
|
+
const auditSkipped = perPackageAudit.every((r) => (r === null || r === void 0 ? void 0 : r.status) === "skipped");
|
|
1664
|
+
if (!opts.quiet || !auditOk) {
|
|
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)"));
|
|
1451
1672
|
}
|
|
1452
1673
|
else {
|
|
1453
|
-
spinner.log(statusLine("✖",
|
|
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"}`));
|
|
1454
1675
|
}
|
|
1455
1676
|
}
|
|
1456
1677
|
if (opts.outdated) {
|
|
1457
1678
|
const outdatedOk = perPackageOutdated.every((r) => r.result && r.result.ok);
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
else {
|
|
1462
|
-
spinner.log(statusLine("✖", `${scanManager.toUpperCase()} outdated data unavailable`));
|
|
1679
|
+
const outdatedSkipped = perPackageOutdated.every((r) => { var _a; return ((_a = r.result) === null || _a === void 0 ? void 0 : _a.status) === "skipped"; });
|
|
1680
|
+
if (!opts.quiet || !outdatedOk) {
|
|
1681
|
+
spinner.log(statusLine(outdatedSkipped ? "ℹ" : outdatedOk ? "✔" : "✖", `${scanManager.toUpperCase()} outdated data ${outdatedSkipped ? "skipped" : outdatedOk ? "collected" : "unavailable"}`));
|
|
1463
1682
|
}
|
|
1464
1683
|
}
|
|
1465
1684
|
const mergedAuditData = mergeAuditResults(perPackageAudit.map((r) => (r && r.ok ? r.data : undefined)));
|
|
@@ -1476,7 +1695,6 @@ async function run() {
|
|
|
1476
1695
|
: undefined;
|
|
1477
1696
|
const npmLsResult = { ok: true, data: mergedGraphData };
|
|
1478
1697
|
const importGraphResult = { ok: true, data: mergedImportGraphData };
|
|
1479
|
-
// Build a merged package.json view for aggregator direct-dep checks.
|
|
1480
1698
|
const mergedPkgForAggregator = mergeDepsFromWorkspace(packageMetas, workspaceClassification.workspacePackageNames, workspaceClassification.localDependencyNames);
|
|
1481
1699
|
const auditFailure = opts.audit
|
|
1482
1700
|
? perPackageAudit.find((r) => r && !r.ok)
|
|
@@ -1491,18 +1709,18 @@ async function run() {
|
|
|
1491
1709
|
if (lsFailures.length > 0) {
|
|
1492
1710
|
const packageList = lsFailures.map((entry) => { var _a; return (_a = entry.meta) === null || _a === void 0 ? void 0 : _a.name; }).filter(Boolean);
|
|
1493
1711
|
spinner.log(`Dependency tree warning: ${lsFailures.length} package${lsFailures.length === 1 ? "" : "s"} failed (${packageList.join(", ")}).`);
|
|
1494
|
-
spinner.log(`First dependency tree error: ${((
|
|
1712
|
+
spinner.log(`First dependency tree error: ${((_b = lsFailures[0].result) === null || _b === void 0 ? void 0 : _b.error) || "pnpm ls failed"}`);
|
|
1495
1713
|
}
|
|
1496
1714
|
if (importFailures.length > 0) {
|
|
1497
1715
|
spinner.log(`Import graph warning: ${importFailures.length} package${importFailures.length === 1 ? "" : "s"} failed (${importFailures[0].error || "import graph failed"})`);
|
|
1498
1716
|
}
|
|
1499
|
-
// Stage 4: aggregate all signals into the final report model.
|
|
1500
1717
|
const aggregated = await (0, aggregator_1.aggregateData)({
|
|
1501
1718
|
projectPath,
|
|
1502
1719
|
auditResult,
|
|
1503
1720
|
npmLsResult,
|
|
1504
1721
|
importGraphResult,
|
|
1505
1722
|
outdatedResult,
|
|
1723
|
+
supplyChainResult,
|
|
1506
1724
|
pkgOverride: mergedPkgForAggregator,
|
|
1507
1725
|
projectPackageJson: rootPkg,
|
|
1508
1726
|
...(projectDependencyPolicy ? { projectDependencyPolicy } : {}),
|
|
@@ -1530,78 +1748,258 @@ async function run() {
|
|
|
1530
1748
|
arch: process.arch,
|
|
1531
1749
|
ci: isCI(),
|
|
1532
1750
|
...(toolVersions ? { toolVersions } : {}),
|
|
1751
|
+
...(typeof opts.targetNodeMajor === "number" ? { targetNodeMajor: opts.targetNodeMajor } : {}),
|
|
1533
1752
|
});
|
|
1534
1753
|
dependencyCount = Object.keys(aggregated.dependencies).length;
|
|
1535
1754
|
const importGraphComplete = perPackageImportGraph.every((result) => result.ok);
|
|
1536
|
-
summary = buildCliSummary(aggregated, {
|
|
1755
|
+
const summary = buildCliSummary(aggregated, {
|
|
1537
1756
|
importGraphComplete,
|
|
1538
1757
|
});
|
|
1539
|
-
policyViolations = (0, failOn_1.evaluatePolicyViolations)(aggregated, opts.failOn);
|
|
1540
|
-
if (workspace.type !== "none") {
|
|
1758
|
+
const policyViolations = (0, failOn_1.evaluatePolicyViolations)(aggregated, opts.failOn);
|
|
1759
|
+
if (!opts.quiet && options.emitWorkspacePackageSummary && workspace.type !== "none") {
|
|
1541
1760
|
console.log(`Detected ${workspace.type.toUpperCase()} workspace with ${packagePaths.length} package${packagePaths.length === 1 ? "" : "s"}.`);
|
|
1542
1761
|
}
|
|
1543
1762
|
if (dependencyCount > 0 && shouldWriteArtifacts) {
|
|
1544
|
-
if (opts.json) {
|
|
1763
|
+
if (opts.format === "json") {
|
|
1545
1764
|
await promises_1.default.mkdir(path_1.default.dirname(outputPath), { recursive: true });
|
|
1546
1765
|
await promises_1.default.writeFile(outputPath, JSON.stringify(aggregated, null, 2), "utf8");
|
|
1547
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
|
+
}
|
|
1548
1779
|
else {
|
|
1549
1780
|
await (0, report_1.renderReport)(aggregated, outputPath);
|
|
1550
1781
|
}
|
|
1551
1782
|
outputCreated = true;
|
|
1552
1783
|
}
|
|
1553
1784
|
spinner.stop(true);
|
|
1554
|
-
const
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
console.log(statusLine("ℹ", "Report output disabled (--no-report); no report artifacts written."));
|
|
1785
|
+
const elapsedSeconds = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
1786
|
+
if (!opts.quiet) {
|
|
1787
|
+
console.log(statusLine("✔", `Scan complete: ${dependencyCount} dependencies analysed in ${elapsedSeconds}s`));
|
|
1558
1788
|
}
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1789
|
+
if (!opts.quiet && options.emitArtifactSummary) {
|
|
1790
|
+
if (!shouldWriteArtifacts) {
|
|
1791
|
+
console.log(statusLine("ℹ", "Report output disabled (--no-report); no report artifacts written."));
|
|
1792
|
+
}
|
|
1793
|
+
else if (outputCreated) {
|
|
1794
|
+
console.log(statusLine("✔", `${formatLabel(opts.format)} written to ${outputPath}`));
|
|
1795
|
+
}
|
|
1796
|
+
else {
|
|
1797
|
+
console.log(statusLine("✖", `No dependencies were found - ${formatLabel(opts.format)} not created`));
|
|
1798
|
+
}
|
|
1564
1799
|
}
|
|
1800
|
+
return {
|
|
1801
|
+
aggregated,
|
|
1802
|
+
summary,
|
|
1803
|
+
policyViolations,
|
|
1804
|
+
dependencyCount,
|
|
1805
|
+
elapsedSeconds,
|
|
1806
|
+
outputCreated,
|
|
1807
|
+
outputPath,
|
|
1808
|
+
shouldWriteArtifacts,
|
|
1809
|
+
collectorAvailability: {
|
|
1810
|
+
audit: !opts.audit
|
|
1811
|
+
? "skipped"
|
|
1812
|
+
: perPackageAudit.every((result) => result && result.ok)
|
|
1813
|
+
? "available"
|
|
1814
|
+
: "unavailable",
|
|
1815
|
+
importGraphComplete,
|
|
1816
|
+
},
|
|
1817
|
+
workspace,
|
|
1818
|
+
packagePaths,
|
|
1819
|
+
};
|
|
1565
1820
|
}
|
|
1566
1821
|
catch (err) {
|
|
1567
1822
|
spinner.stop(false);
|
|
1568
|
-
|
|
1569
|
-
process.exit(1);
|
|
1823
|
+
throw err;
|
|
1570
1824
|
}
|
|
1571
1825
|
finally {
|
|
1572
1826
|
if (shouldWriteArtifacts) {
|
|
1573
1827
|
if (!opts.keepTemp) {
|
|
1574
1828
|
await (0, utils_1.removeDir)(tempDir);
|
|
1575
1829
|
}
|
|
1576
|
-
else {
|
|
1830
|
+
else if (!opts.quiet) {
|
|
1577
1831
|
console.log(statusLine("✔", `Temporary data kept at ${tempDir}`));
|
|
1578
1832
|
}
|
|
1579
1833
|
}
|
|
1580
1834
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1835
|
+
}
|
|
1836
|
+
async function runScanCommand(opts) {
|
|
1837
|
+
const result = await executeAnalysis(opts, {
|
|
1838
|
+
shouldWriteArtifacts: !opts.noReport,
|
|
1839
|
+
emitArtifactSummary: true,
|
|
1840
|
+
emitWorkspacePackageSummary: true,
|
|
1841
|
+
});
|
|
1842
|
+
if (!opts.quiet) {
|
|
1843
|
+
if (opts.open && !result.shouldWriteArtifacts) {
|
|
1844
|
+
console.log(statusLine("✖", "Skipping auto-open because --no-report is enabled."));
|
|
1845
|
+
}
|
|
1846
|
+
else if (opts.open && result.outputCreated && !isCI()) {
|
|
1847
|
+
console.log(statusLine("↗", `Opening ${path_1.default.basename(result.outputPath)} using system default ${opts.format === "html" ? "browser" : "application"}.`));
|
|
1848
|
+
openInBrowser(result.outputPath);
|
|
1849
|
+
}
|
|
1850
|
+
else if (opts.open && result.outputCreated && isCI()) {
|
|
1851
|
+
console.log(statusLine("✖", "Skipping auto-open in CI environment."));
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
printCliSummary(result.summary);
|
|
1855
|
+
printPolicyViolations(result.policyViolations);
|
|
1856
|
+
if (!opts.quiet) {
|
|
1857
|
+
console.log(`Enrich this scan with maintenance signals, upgrade readiness, and risk modelling at ${formatTerminalLink("https://www.dependency-radar.com", "https://www.dependency-radar.com")}`);
|
|
1858
|
+
}
|
|
1859
|
+
if (result.policyViolations.length > 0) {
|
|
1860
|
+
process.exit(1);
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
async function runExplainCommand(opts) {
|
|
1864
|
+
var _a;
|
|
1865
|
+
const packageName = (_a = opts.packageName) === null || _a === void 0 ? void 0 : _a.trim();
|
|
1866
|
+
if (!packageName) {
|
|
1867
|
+
console.error("Missing package name for explain. Usage: dependency-radar explain <package-name>");
|
|
1868
|
+
process.exit(1);
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
const result = await executeAnalysis(opts, {
|
|
1872
|
+
shouldWriteArtifacts: false,
|
|
1873
|
+
emitArtifactSummary: false,
|
|
1874
|
+
emitWorkspacePackageSummary: false,
|
|
1875
|
+
});
|
|
1876
|
+
const matches = (0, explain_1.findDependenciesByPackageName)(result.aggregated, packageName);
|
|
1877
|
+
console.log("");
|
|
1878
|
+
console.log((0, explain_1.formatExplainOutput)(packageName, matches, {
|
|
1879
|
+
audit: result.collectorAvailability.audit,
|
|
1880
|
+
importGraphComplete: result.collectorAvailability.importGraphComplete,
|
|
1881
|
+
}));
|
|
1882
|
+
if (matches.length === 0) {
|
|
1883
|
+
process.exit(1);
|
|
1884
|
+
}
|
|
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;
|
|
1583
1913
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
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;
|
|
1587
1930
|
}
|
|
1588
|
-
|
|
1589
|
-
console.
|
|
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;
|
|
1590
1935
|
}
|
|
1591
|
-
|
|
1592
|
-
|
|
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
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Run the CLI entrypoint and dispatch to the selected command.
|
|
1946
|
+
*/
|
|
1947
|
+
async function run() {
|
|
1948
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
1949
|
+
if (opts.invalidCommand) {
|
|
1950
|
+
printHelp();
|
|
1951
|
+
process.exit(1);
|
|
1952
|
+
return;
|
|
1593
1953
|
}
|
|
1594
|
-
|
|
1595
|
-
|
|
1954
|
+
try {
|
|
1955
|
+
if ((opts.schema && !opts.commandProvided) || opts.command === "schema") {
|
|
1956
|
+
await runSchemaCommand(opts);
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
if (opts.command === "explain") {
|
|
1960
|
+
await runExplainCommand(opts);
|
|
1961
|
+
return;
|
|
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
|
+
}
|
|
1971
|
+
await runScanCommand(opts);
|
|
1596
1972
|
}
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
console.log("Enrich this scan with maintenance signals, upgrade readiness, and risk modelling at dependency-radar.com");
|
|
1600
|
-
if (policyViolations.length > 0) {
|
|
1973
|
+
catch (err) {
|
|
1974
|
+
console.error("Failed to generate report:", err);
|
|
1601
1975
|
process.exit(1);
|
|
1602
1976
|
}
|
|
1603
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
|
+
}
|
|
1604
1991
|
run();
|
|
1992
|
+
function createProgressReporter(text, quiet) {
|
|
1993
|
+
if (!quiet)
|
|
1994
|
+
return startSpinner(text);
|
|
1995
|
+
return {
|
|
1996
|
+
stop: () => { },
|
|
1997
|
+
update: () => { },
|
|
1998
|
+
log: (line) => {
|
|
1999
|
+
process.stdout.write(`${colorLeadingSymbol(line)}\n`);
|
|
2000
|
+
},
|
|
2001
|
+
};
|
|
2002
|
+
}
|
|
1605
2003
|
/**
|
|
1606
2004
|
* Displays a rotating CLI spinner with a message and returns controls to stop, update, or log lines.
|
|
1607
2005
|
*
|